From a2b87f48775ca6687de0eb4af4b846bd1f0cdebf Mon Sep 17 00:00:00 2001 From: "Brian S. O'Neill" Date: Sun, 14 Oct 2007 02:31:22 +0000 Subject: Added support for "where exists" in queries via new syntax. --- src/main/java/com/amazon/carbonado/Storage.java | 2 + .../amazon/carbonado/cursor/FilteredCursor.java | 12 +- .../carbonado/cursor/FilteredCursorGenerator.java | 253 ++++++++++++++++---- .../cursor/PropertyOrdinalMapVisitor.java | 58 +++++ .../carbonado/cursor/ShortCircuitOptimizer.java | 11 + .../com/amazon/carbonado/filter/AndFilter.java | 12 +- .../amazon/carbonado/filter/BinaryOpFilter.java | 2 +- .../java/com/amazon/carbonado/filter/Binder.java | 23 +- .../com/amazon/carbonado/filter/ClosedFilter.java | 12 +- .../com/amazon/carbonado/filter/Distributer.java | 14 ++ .../com/amazon/carbonado/filter/ExistsFilter.java | 264 +++++++++++++++++++++ .../java/com/amazon/carbonado/filter/Filter.java | 180 +++++++++++--- .../com/amazon/carbonado/filter/FilterParser.java | 59 ++++- .../java/com/amazon/carbonado/filter/Group.java | 8 + .../com/amazon/carbonado/filter/OpenFilter.java | 12 +- .../java/com/amazon/carbonado/filter/OrFilter.java | 18 +- .../amazon/carbonado/filter/PropertyFilter.java | 14 +- .../carbonado/filter/PropertyFilterList.java | 17 ++ .../java/com/amazon/carbonado/filter/Reducer.java | 11 + .../java/com/amazon/carbonado/filter/Visitor.java | 7 + .../com/amazon/carbonado/info/ChainedProperty.java | 10 +- .../java/com/amazon/carbonado/qe/EmptyQuery.java | 5 +- .../amazon/carbonado/qe/FilteredQueryExecutor.java | 10 +- .../amazon/carbonado/qe/JoinedQueryExecutor.java | 3 +- .../java/com/amazon/carbonado/qe/QueryEngine.java | 17 +- .../java/com/amazon/carbonado/qe/QueryFactory.java | 7 +- .../com/amazon/carbonado/qe/StandardQuery.java | 89 ++++--- .../amazon/carbonado/qe/StandardQueryFactory.java | 30 ++- .../amazon/carbonado/qe/UnionQueryAnalyzer.java | 12 + .../amazon/carbonado/repo/jdbc/JDBCStorage.java | 194 ++++++++++++--- 30 files changed, 1138 insertions(+), 228 deletions(-) create mode 100644 src/main/java/com/amazon/carbonado/cursor/PropertyOrdinalMapVisitor.java create mode 100644 src/main/java/com/amazon/carbonado/filter/ExistsFilter.java (limited to 'src/main') diff --git a/src/main/java/com/amazon/carbonado/Storage.java b/src/main/java/com/amazon/carbonado/Storage.java index bb4b8b7..551e1eb 100644 --- a/src/main/java/com/amazon/carbonado/Storage.java +++ b/src/main/java/com/amazon/carbonado/Storage.java @@ -86,10 +86,12 @@ public interface Storage { * AndFilter = NotFilter { "&" NotFilter } * NotFilter = [ "!" ] EntityFilter * EntityFilter = PropertyFilter + * = ChainedFilter * | "(" Filter ")" * PropertyFilter = ChainedProperty RelOp "?" * RelOp = "=" | "!=" | "<" | ">=" | ">" | "<=" * ChainedProperty = Identifier { "." Identifier } + * ChainedFilter = ChainedProperty "(" [ Filter ] ")" * * * @param filter query filter expression diff --git a/src/main/java/com/amazon/carbonado/cursor/FilteredCursor.java b/src/main/java/com/amazon/carbonado/cursor/FilteredCursor.java index 51d69e7..59cb941 100644 --- a/src/main/java/com/amazon/carbonado/cursor/FilteredCursor.java +++ b/src/main/java/com/amazon/carbonado/cursor/FilteredCursor.java @@ -25,10 +25,8 @@ import com.amazon.carbonado.FetchException; import com.amazon.carbonado.FetchInterruptedException; import com.amazon.carbonado.Storable; -import com.amazon.carbonado.filter.ClosedFilter; import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.FilterValues; -import com.amazon.carbonado.filter.OpenFilter; /** * Wraps another cursor and applies custom filtering to reduce the set of @@ -66,7 +64,7 @@ public abstract class FilteredCursor extends AbstractCursor { * IllegalStateException will result otherwise. * * @param filter filter to apply - * @param filterValues values for filter + * @param filterValues values for filter, which may be null if filter has no parameters * @param cursor cursor to wrap * @return wrapped cursor which filters results * @throws IllegalStateException if any values are not specified @@ -76,18 +74,18 @@ public abstract class FilteredCursor extends AbstractCursor { FilterValues filterValues, Cursor cursor) { - if (filter instanceof OpenFilter) { + if (filter.isOpen()) { return cursor; } - if (filter instanceof ClosedFilter) { + if (filter.isClosed()) { throw new IllegalArgumentException(); } // Make sure the filter is the same one that filterValues should be using. filter = filter.bind(); - return FilteredCursorGenerator.getFactory(filter) - .newFilteredCursor(cursor, filterValues.getValuesFor(filter)); + Object[] values = filterValues == null ? null : filterValues.getValuesFor(filter); + return FilteredCursorGenerator.getFactory(filter).newFilteredCursor(cursor, values); } private final Cursor mCursor; diff --git a/src/main/java/com/amazon/carbonado/cursor/FilteredCursorGenerator.java b/src/main/java/com/amazon/carbonado/cursor/FilteredCursorGenerator.java index 9ca0b79..745ad7a 100644 --- a/src/main/java/com/amazon/carbonado/cursor/FilteredCursorGenerator.java +++ b/src/main/java/com/amazon/carbonado/cursor/FilteredCursorGenerator.java @@ -18,8 +18,13 @@ package com.amazon.carbonado.cursor; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; + +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Stack; @@ -38,9 +43,11 @@ import org.cojen.util.ClassInjector; import org.cojen.util.WeakIdentityMap; import com.amazon.carbonado.Cursor; +import com.amazon.carbonado.Query; import com.amazon.carbonado.Storable; import com.amazon.carbonado.filter.AndFilter; +import com.amazon.carbonado.filter.ExistsFilter; import com.amazon.carbonado.filter.OrFilter; import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.PropertyFilter; @@ -50,8 +57,11 @@ import com.amazon.carbonado.filter.Visitor; import com.amazon.carbonado.info.ChainedProperty; import com.amazon.carbonado.info.StorableProperty; +import com.amazon.carbonado.util.ThrowUnchecked; import com.amazon.carbonado.util.QuickConstructorGenerator; +import com.amazon.carbonado.gen.CodeBuilderUtil; + /** * Generates Cursor implementations that wrap another Cursor, applying a @@ -61,6 +71,8 @@ import com.amazon.carbonado.util.QuickConstructorGenerator; * @see FilteredCursor */ class FilteredCursorGenerator { + private static final String SUB_FILTER_INIT_METHOD = "subFilterInit$"; + private static Map cCache = new WeakIdentityMap(); /** @@ -72,11 +84,6 @@ class FilteredCursorGenerator { */ @SuppressWarnings("unchecked") static Factory getFactory(Filter filter) { - return getFactory(filter, false); - } - - @SuppressWarnings("unchecked") - private static Factory getFactory(Filter filter, boolean optimize) { if (filter == null) { throw new IllegalArgumentException(); } @@ -85,16 +92,8 @@ class FilteredCursorGenerator { if (factory != null) { return factory; } - - Filter optimized; - if (optimize && (optimized = ShortCircuitOptimizer.optimize(filter)) != filter) { - // Use factory for filter optimized for short-circuit logic. - factory = getFactory(optimized, false); - } else { - Class> clazz = generateClass(filter); - factory = QuickConstructorGenerator.getInstance(clazz, Factory.class); - } - + Class> clazz = generateClass(filter); + factory = QuickConstructorGenerator.getInstance(clazz, Factory.class); cCache.put(filter, factory); return factory; } @@ -145,7 +144,7 @@ class FilteredCursorGenerator { CodeBuilder isAllowedBuilder; LocalVariable storableVar; { - TypeDesc[] params = {TypeDesc.OBJECT}; + TypeDesc[] params = {OBJECT}; MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, "isAllowed", BOOLEAN, params); isAllowedBuilder = new CodeBuilder(mi); @@ -167,12 +166,38 @@ class FilteredCursorGenerator { isAllowedBuilder.storeLocal(storableVar); } - filter.accept(new CodeGen(cf, ctorBuilder, isAllowedBuilder, storableVar), null); + // Capture property filter ordinals before optimization scrambles them. + Map propertyOrdinalMap; + { + PropertyOrdinalMapVisitor visitor = new PropertyOrdinalMapVisitor(); + filter.accept(visitor, null); + propertyOrdinalMap = visitor.getPropertyOrdinalMap(); + } + + filter = ShortCircuitOptimizer.optimize(filter); + + CodeGen cg = new CodeGen + (propertyOrdinalMap, cf, ctorBuilder, isAllowedBuilder, storableVar); + filter.accept(cg, null); + + List subFilters = cg.finishSubFilterInit(); // Finish constructor. ctorBuilder.returnVoid(); - return (Class>) ci.defineClass(cf); + Class generated = ci.defineClass(cf); + + // Pass sub-filter instances to be stored in static fields. + if (subFilters != null && subFilters.size() > 0) { + try { + Method init = generated.getMethod(SUB_FILTER_INIT_METHOD, Filter[].class); + init.invoke(null, (Object) subFilters.toArray(new Filter[subFilters.size()])); + } catch (Exception e) { + ThrowUnchecked.fireDeclaredRootCause(e); + } + } + + return (Class>) generated; } public static interface Factory { @@ -235,8 +260,10 @@ class FilteredCursorGenerator { } private static class CodeGen extends Visitor { - private static String FIELD_PREFIX = "value$"; + private static final String FIELD_PREFIX = "value$"; + private static final String FILTER_FIELD_PREFIX = "filter$"; + private final Map mPropertyOrdinalMap; private final ClassFile mClassFile; private final CodeBuilder mCtorBuilder; private final CodeBuilder mIsAllowedBuilder; @@ -244,11 +271,16 @@ class FilteredCursorGenerator { private final Stack mScopeStack; - private int mPropertyOrdinal; + private List mSubFilters; + private CodeBuilder mSubFilterInitBuilder; - CodeGen(ClassFile cf, + CodeGen(Map propertyOrdinalMap, + ClassFile cf, CodeBuilder ctorBuilder, - CodeBuilder isAllowedBuilder, LocalVariable storableVar) { + CodeBuilder isAllowedBuilder, + LocalVariable storableVar) + { + mPropertyOrdinalMap = propertyOrdinalMap; mClassFile = cf; mCtorBuilder = ctorBuilder; mIsAllowedBuilder = isAllowedBuilder; @@ -257,6 +289,16 @@ class FilteredCursorGenerator { mScopeStack.push(new Scope(null, null)); } + public List finishSubFilterInit() { + if (mSubFilterInitBuilder != null) { + mSubFilterInitBuilder.returnVoid(); + } + if (mSubFilters == null) { + return Collections.emptyList(); + } + return mSubFilters; + } + public Object visit(OrFilter filter, Object param) { Label failLocation = mIsAllowedBuilder.createLabel(); // Inherit success location to short-circuit if 'or' test succeeds. @@ -280,26 +322,144 @@ class FilteredCursorGenerator { } public Object visit(PropertyFilter filter, Object param) { - ChainedProperty chained = filter.getChainedProperty(); - TypeDesc type = TypeDesc.forClass(chained.getType()); - TypeDesc fieldType = actualFieldType(type); - String fieldName = FIELD_PREFIX + mPropertyOrdinal; + final int propertyOrdinal = mPropertyOrdinalMap.get(filter); + final TypeDesc type = TypeDesc.forClass(filter.getChainedProperty().getType()); + final TypeDesc fieldType = actualFieldType(type); + final String fieldName = FIELD_PREFIX + propertyOrdinal; // Define storage field. mClassFile.addField(Modifiers.PRIVATE.toFinal(true), fieldName, fieldType); // Add code to constructor to store value into field. - CodeBuilder b = mCtorBuilder; - b.loadThis(); - b.loadLocal(b.getParameter(1)); - b.loadConstant(mPropertyOrdinal); - b.loadFromArray(OBJECT); - b.checkCast(type.toObjectType()); - convertProperty(b, type.toObjectType(), fieldType); - b.storeField(fieldName, fieldType); - - // Add code to load property value to stack. - b = mIsAllowedBuilder; + { + CodeBuilder b = mCtorBuilder; + b.loadThis(); + b.loadLocal(b.getParameter(1)); + b.loadConstant(propertyOrdinal); + b.loadFromArray(OBJECT); + b.checkCast(type.toObjectType()); + convertProperty(b, type.toObjectType(), fieldType); + b.storeField(fieldName, fieldType); + } + + loadChainedProperty(mIsAllowedBuilder, filter.getChainedProperty()); + addPropertyFilter(mIsAllowedBuilder, propertyOrdinal, type, filter.getOperator()); + + return null; + } + + public Object visit(ExistsFilter filter, Object param) { + // Recursively gather all the properties to be passed to sub-filter. + final List subPropFilters = new ArrayList(); + + filter.getSubFilter().accept(new Visitor() { + @Override + public Object visit(PropertyFilter filter, Object param) { + subPropFilters.add(filter); + return null; + } + @Override + public Object visit(ExistsFilter filter, Object param) { + return filter.getSubFilter().accept(this, param); + } + }, null); + + // Load join property value to stack. It is expected to be a Query. + CodeBuilder b = mIsAllowedBuilder; + loadChainedProperty(b, filter.getChainedProperty()); + + final TypeDesc queryType = TypeDesc.forClass(Query.class); + + // Refine Query filter, if sub-filter isn't open. + if (!filter.getSubFilter().isOpen()) { + String subFilterFieldName = addStaticFilterField(filter.getSubFilter()); + + TypeDesc filterType = TypeDesc.forClass(Filter.class); + + b.loadStaticField(subFilterFieldName, filterType); + b.invokeInterface(queryType, "and", queryType, new TypeDesc[] {filterType}); + + for (PropertyFilter subPropFilter : subPropFilters) { + final int propertyOrdinal = mPropertyOrdinalMap.get(subPropFilter); + final ChainedProperty chained = subPropFilter.getChainedProperty(); + final String fieldName = FIELD_PREFIX + propertyOrdinal; + + // Define storage for sub-filter. + mClassFile.addField(Modifiers.PRIVATE.toFinal(true), fieldName, OBJECT); + + // Assign value passed from constructor. + mCtorBuilder.loadThis(); + mCtorBuilder.loadLocal(mCtorBuilder.getParameter(1)); + mCtorBuilder.loadConstant(propertyOrdinal); + mCtorBuilder.loadFromArray(OBJECT); + mCtorBuilder.storeField(fieldName, OBJECT); + + // Pass value to Query. + b.loadThis(); + b.loadField(fieldName, OBJECT); + b.invokeInterface(queryType, "with", queryType, new TypeDesc[] {OBJECT}); + } + } + + // Call the all-important Query.exists method. + b.invokeInterface(queryType, "exists", BOOLEAN, null); + + // Success if boolean value is true (non-zero), opposite for "not exists". + RelOp op = filter.isNotExists() ? RelOp.EQ : RelOp.NE; + getScope().successIfZeroComparisonElseFail(b, op); + + return null; + } + + private String addStaticFilterField(Filter filter) { + if (mSubFilters == null) { + mSubFilters = new ArrayList(); + } + + final int filterOrdinal = mSubFilters.size(); + final String fieldName = FILTER_FIELD_PREFIX + filterOrdinal; + final TypeDesc filterType = TypeDesc.forClass(Filter.class); + + mClassFile.addField(Modifiers.PRIVATE.toStatic(true), fieldName, filterType); + + mSubFilters.add(filter); + + if (mSubFilterInitBuilder == null) { + TypeDesc filterArrayType = filterType.toArrayType(); + mSubFilterInitBuilder = new CodeBuilder + (mClassFile.addMethod + (Modifiers.PUBLIC.toStatic(true), + SUB_FILTER_INIT_METHOD, null, new TypeDesc[] {filterArrayType})); + + // This method must be public, so add a check to ensure it is + // called at most once. Just check one filter to see if it is non-null. + + mSubFilterInitBuilder.loadStaticField(fieldName, filterType); + Label isNull = mSubFilterInitBuilder.createLabel(); + mSubFilterInitBuilder.ifNullBranch(isNull, true); + CodeBuilderUtil.throwException + (mSubFilterInitBuilder, IllegalStateException.class, null); + isNull.setLocation(); + } + + // Now add code to init field later. + mSubFilterInitBuilder.loadLocal(mSubFilterInitBuilder.getParameter(0)); + mSubFilterInitBuilder.loadConstant(filterOrdinal); + mSubFilterInitBuilder.loadFromArray(filterType); + mSubFilterInitBuilder.storeStaticField(fieldName, filterType); + + return fieldName; + } + + private Scope getScope() { + return mScopeStack.peek(); + } + + /** + * Generated code checks if chained properties resolve to null, and if + * so, branches to the current scope's fail location. + */ + private void loadChainedProperty(CodeBuilder b, ChainedProperty chained) { b.loadLocal(mStorableVar); loadProperty(b, chained.getPrimeProperty()); for (int i=0; i property) { @@ -341,9 +492,9 @@ class FilteredCursorGenerator { b.invoke(readMethod); } - private void addPropertyFilter(CodeBuilder b, TypeDesc type, RelOp relOp) { + private void addPropertyFilter(CodeBuilder b, int ordinal, TypeDesc type, RelOp relOp) { TypeDesc fieldType = actualFieldType(type); - String fieldName = FIELD_PREFIX + mPropertyOrdinal; + String fieldName = FIELD_PREFIX + ordinal; if (type.getTypeCode() == OBJECT_CODE) { // Check if actual property being examined is null. @@ -524,7 +675,7 @@ class FilteredCursorGenerator { private void convertProperty(CodeBuilder b, TypeDesc fromType, TypeDesc toType) { TypeDesc fromPrimType = fromType.toPrimitiveType(); - if (fromPrimType != TypeDesc.FLOAT && fromPrimType != TypeDesc.DOUBLE) { + if (fromPrimType != FLOAT && fromPrimType != DOUBLE) { // Not converting floating point, so just convert as normal. b.convert(fromType, toType); return; @@ -532,7 +683,7 @@ class FilteredCursorGenerator { TypeDesc toPrimType = toType.toPrimitiveType(); - if (toPrimType != TypeDesc.INT && toPrimType != TypeDesc.LONG) { + if (toPrimType != INT && toPrimType != LONG) { // Floating point not being converted to bits, so just convert as normal. b.convert(fromType, toType); return; @@ -557,7 +708,7 @@ class FilteredCursorGenerator { // Floating point bits need to be flipped for negative values. - if (toPrimType == TypeDesc.INT) { + if (toPrimType == INT) { b.dup(); b.ifZeroComparisonBranch(box, ">="); b.loadConstant(0x7fffffff); diff --git a/src/main/java/com/amazon/carbonado/cursor/PropertyOrdinalMapVisitor.java b/src/main/java/com/amazon/carbonado/cursor/PropertyOrdinalMapVisitor.java new file mode 100644 index 0000000..8bcfa52 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/cursor/PropertyOrdinalMapVisitor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2007 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.util.IdentityHashMap; +import java.util.Map; + +import com.amazon.carbonado.Storable; + +import com.amazon.carbonado.filter.ExistsFilter; +import com.amazon.carbonado.filter.PropertyFilter; +import com.amazon.carbonado.filter.Visitor; + +/** + * Visits a filter and maps all property filters to their zero-based ordinal + * position. + * + * @author Brian S O'Neill + */ +class PropertyOrdinalMapVisitor extends Visitor { + private int mOrdinal; + private Map mOrdinalMap; + + PropertyOrdinalMapVisitor() { + mOrdinalMap = new IdentityHashMap(); + } + + public Map getPropertyOrdinalMap() { + return mOrdinalMap; + } + + @Override + public Object visit(PropertyFilter filter, Object param) { + mOrdinalMap.put(filter, mOrdinal++); + return null; + } + + @Override + public Object visit(ExistsFilter filter, Object param) { + return filter.getSubFilter().accept(this, param); + } +} diff --git a/src/main/java/com/amazon/carbonado/cursor/ShortCircuitOptimizer.java b/src/main/java/com/amazon/carbonado/cursor/ShortCircuitOptimizer.java index 1a46d1e..46b03b1 100644 --- a/src/main/java/com/amazon/carbonado/cursor/ShortCircuitOptimizer.java +++ b/src/main/java/com/amazon/carbonado/cursor/ShortCircuitOptimizer.java @@ -25,6 +25,7 @@ import com.amazon.carbonado.info.StorableProperty; import com.amazon.carbonado.filter.AndFilter; import com.amazon.carbonado.filter.ClosedFilter; +import com.amazon.carbonado.filter.ExistsFilter; import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.OpenFilter; import com.amazon.carbonado.filter.OrFilter; @@ -132,6 +133,7 @@ class ShortCircuitOptimizer { } private static class Walker extends Visitor, Object> { + @Override public FilterAndCost visit(OrFilter filter, Object param) { FilterAndCost leftCost = filter.getLeftFilter().accept(this, param); FilterAndCost rightCost = filter.getRightFilter().accept(this, param); @@ -154,6 +156,7 @@ class ShortCircuitOptimizer { return new FilterAndCost(newFilter, expensiveProperty); } + @Override public FilterAndCost visit(AndFilter filter, Object param) { FilterAndCost leftCost = filter.getLeftFilter().accept(this, param); FilterAndCost rightCost = filter.getRightFilter().accept(this, param); @@ -176,14 +179,22 @@ class ShortCircuitOptimizer { return new FilterAndCost(newFilter, expensiveProperty); } + @Override public FilterAndCost visit(PropertyFilter filter, Object param) { return new FilterAndCost(filter, filter.getChainedProperty()); } + @Override + public FilterAndCost visit(ExistsFilter filter, Object param) { + return new FilterAndCost(filter, filter.getChainedProperty()); + } + + @Override public FilterAndCost visit(OpenFilter filter, Object param) { return new FilterAndCost(filter, null); } + @Override public FilterAndCost visit(ClosedFilter filter, Object param) { return new FilterAndCost(filter, null); } diff --git a/src/main/java/com/amazon/carbonado/filter/AndFilter.java b/src/main/java/com/amazon/carbonado/filter/AndFilter.java index be38800..a260b8c 100644 --- a/src/main/java/com/amazon/carbonado/filter/AndFilter.java +++ b/src/main/java/com/amazon/carbonado/filter/AndFilter.java @@ -62,16 +62,14 @@ public class AndFilter extends BinaryOpFilter { return mLeft.unbind().and(mRight.unbind()); } - public Filter asJoinedFrom(ChainedProperty joinProperty) { - return mLeft.asJoinedFrom(joinProperty).and(mRight.asJoinedFrom(joinProperty)); + Filter asJoinedFromAny(ChainedProperty joinProperty) { + return mLeft.asJoinedFromAny(joinProperty).and(mRight.asJoinedFromAny(joinProperty)); } @Override - NotJoined notJoinedFrom(ChainedProperty joinProperty, - Class joinPropertyType) - { - NotJoined left = mLeft.notJoinedFrom(joinProperty, joinPropertyType); - NotJoined right = mRight.notJoinedFrom(joinProperty, joinPropertyType); + NotJoined notJoinedFromCNF(ChainedProperty joinProperty) { + NotJoined left = mLeft.notJoinedFromCNF(joinProperty); + NotJoined right = mRight.notJoinedFromCNF(joinProperty); // Remove wildcards to shut the compiler up. Filter leftNotJoined = left.getNotJoinedFilter(); diff --git a/src/main/java/com/amazon/carbonado/filter/BinaryOpFilter.java b/src/main/java/com/amazon/carbonado/filter/BinaryOpFilter.java index d17d466..640c0ae 100644 --- a/src/main/java/com/amazon/carbonado/filter/BinaryOpFilter.java +++ b/src/main/java/com/amazon/carbonado/filter/BinaryOpFilter.java @@ -42,7 +42,7 @@ public abstract class BinaryOpFilter extends Filter { BinaryOpFilter(Filter left, Filter right) { super(left == null ? null : left.getStorableType()); if (left == null || right == null) { - throw new IllegalArgumentException(); + throw new IllegalArgumentException("Left or right filter is null"); } if (left.getStorableType() != right.getStorableType()) { throw new IllegalArgumentException("Type mismatch"); diff --git a/src/main/java/com/amazon/carbonado/filter/Binder.java b/src/main/java/com/amazon/carbonado/filter/Binder.java index 7a854f1..7b074c2 100644 --- a/src/main/java/com/amazon/carbonado/filter/Binder.java +++ b/src/main/java/com/amazon/carbonado/filter/Binder.java @@ -39,6 +39,10 @@ class Binder extends Visitor, Object> { mBindMap = new IdentityHashMap, PropertyFilter>(); } + private Binder(Map, PropertyFilter> bindMap) { + mBindMap = bindMap; + } + @Override public Filter visit(OrFilter filter, Object param) { Filter left = filter.getLeftFilter(); @@ -77,7 +81,7 @@ class Binder extends Visitor, Object> { @Override public Filter visit(PropertyFilter filter, Object param) { - if (filter.getBindID() != 0) { + if (filter.isBound()) { return filter; } filter = PropertyFilter.getCanonical(filter, 1); @@ -90,4 +94,21 @@ class Binder extends Visitor, Object> { mBindMap.put(filter, highest); return highest; } + + @Override + public Filter visit(ExistsFilter filter, Object param) { + if (filter.isBound()) { + return filter; + } + Filter boundJoinedSubFilter = + filter.getJoinedSubFilter().accept(new Binder(mBindMap), null); + Filter.NotJoined nj = + boundJoinedSubFilter.notJoinedFromAny(filter.getChainedProperty()); + if (nj.getRemainderFilter() != null && !(nj.getRemainderFilter().isOpen())) { + // This should not happen. + throw new IllegalStateException(nj.toString()); + } + return ExistsFilter.getCanonical + (filter.getChainedProperty(), nj.getNotJoinedFilter(), filter.isNotExists()); + } } diff --git a/src/main/java/com/amazon/carbonado/filter/ClosedFilter.java b/src/main/java/com/amazon/carbonado/filter/ClosedFilter.java index 598f149..0630bab 100644 --- a/src/main/java/com/amazon/carbonado/filter/ClosedFilter.java +++ b/src/main/java/com/amazon/carbonado/filter/ClosedFilter.java @@ -36,6 +36,16 @@ public class ClosedFilter extends Filter { super(type); } + /** + * Always returns true. + * + * @since 1.2 + */ + @Override + public final boolean isClosed() { + return true; + } + public ClosedFilter and(Filter filter) { return this; } @@ -92,7 +102,7 @@ public class ClosedFilter extends Filter { return true; } - public ClosedFilter asJoinedFrom(ChainedProperty joinProperty) { + ClosedFilter asJoinedFromAny(ChainedProperty joinProperty) { return getClosedFilter(joinProperty.getPrimeProperty().getEnclosingType()); } diff --git a/src/main/java/com/amazon/carbonado/filter/Distributer.java b/src/main/java/com/amazon/carbonado/filter/Distributer.java index e47e842..2aa9a2c 100644 --- a/src/main/java/com/amazon/carbonado/filter/Distributer.java +++ b/src/main/java/com/amazon/carbonado/filter/Distributer.java @@ -75,4 +75,18 @@ class Distributer extends Visitor, Filter> { return mDoAnd ? distribute.and(filter) : distribute.or(filter); } } + + /** + * @param filter candidate node to potentially replace + * @param distribute node to distribute into candidate node + * @return original candidate or replacement + */ + @Override + public Filter visit(ExistsFilter filter, Filter distribute) { + if (mDoRight) { + return mDoAnd ? filter.and(distribute) : filter.or(distribute); + } else { + return mDoAnd ? distribute.and(filter) : distribute.or(filter); + } + } } diff --git a/src/main/java/com/amazon/carbonado/filter/ExistsFilter.java b/src/main/java/com/amazon/carbonado/filter/ExistsFilter.java new file mode 100644 index 0000000..b75de28 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/filter/ExistsFilter.java @@ -0,0 +1,264 @@ +/* + * Copyright 2007 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.filter; + +import java.io.IOException; + +import com.amazon.carbonado.Query; +import com.amazon.carbonado.Storable; + +import com.amazon.carbonado.info.ChainedProperty; +import com.amazon.carbonado.info.StorableProperty; + +/** + * Filter tree node that performs an existence or non-existence test against a + * one-to-many join. + * + * @author Brian S O'Neill + * @since 1.2 + */ +public class ExistsFilter extends Filter { + /** + * Returns a canonical instance, creating a new one if there isn't one + * already in the cache. + */ + @SuppressWarnings("unchecked") + static ExistsFilter getCanonical(ChainedProperty property, + Filter subFilter, + boolean not) + { + return (ExistsFilter) cCanonical.put(new ExistsFilter(property, subFilter, not)); + } + + private final ChainedProperty mProperty; + private final Filter mSubFilter; + private final boolean mNot; + + private transient volatile Filter mJoinedSubFilter; + private transient volatile boolean mNoParameters; + + ExistsFilter(ChainedProperty property, Filter subFilter, boolean not) { + super(property == null ? null : property.getPrimeProperty().getEnclosingType()); + + StorableProperty joinProperty = property.getLastProperty(); + if (!joinProperty.isQuery()) { + throw new IllegalArgumentException("Not a one-to-many join property: " + property); + } + if (subFilter == null) { + subFilter = Filter.getOpenFilter(joinProperty.getJoinedType()); + } else if (subFilter.isClosed()) { + throw new IllegalArgumentException("Exists sub-filter cannot be closed: " + subFilter); + } else if (joinProperty.getJoinedType() != subFilter.getStorableType()) { + throw new IllegalArgumentException + ("Filter not compatible with join property type: " + + property + " joins to a " + joinProperty.getJoinedType().getName() + + ", but filter is for a " + subFilter.getStorableType().getName()); + } + + mProperty = property; + mSubFilter = subFilter; + mNot = not; + } + + /** + * @return chained property whose last property is a one-to-many join + */ + public ChainedProperty getChainedProperty() { + return mProperty; + } + + /** + * @return filter which is applied to last property of chain, which might be open + */ + public Filter getSubFilter() { + return mSubFilter; + } + + Filter getJoinedSubFilter() { + Filter joined = mJoinedSubFilter; + if (joined == null) { + mJoinedSubFilter = joined = mSubFilter.asJoinedFromAny(mProperty); + } + return joined; + } + + /** + * @return true if this filter is testing for "not exists" + */ + public boolean isNotExists() { + return mNot; + } + + public Filter not() { + return getCanonical(mProperty, mSubFilter, !mNot); + } + + @Override + public FilterValues initialFilterValues() { + if (mNoParameters) { + return null; + } + FilterValues filterValues = super.initialFilterValues(); + if (filterValues == null) { + // Avoid cost of discovering this the next time. + mNoParameters = true; + } + return filterValues; + } + + @Override + PropertyFilterList getTailPropertyFilterList() { + if (mNoParameters) { + return null; + } + PropertyFilterList tail = super.getTailPropertyFilterList(); + if (tail == null) { + // Avoid cost of discovering this the next time. + mNoParameters = true; + } + return tail; + } + + public R accept(Visitor visitor, P param) { + return visitor.visit(this, param); + } + + public ExistsFilter bind() { + Filter boundSubFilter = mSubFilter.bind(); + if (boundSubFilter == mSubFilter) { + return this; + } + return getCanonical(mProperty, boundSubFilter, mNot); + } + + public ExistsFilter unbind() { + Filter unboundSubFilter = mSubFilter.unbind(); + if (unboundSubFilter == mSubFilter) { + return this; + } + return getCanonical(mProperty, unboundSubFilter, mNot); + } + + public boolean isBound() { + return mSubFilter.isBound(); + } + + void markBound() { + } + + ExistsFilter asJoinedFromAny(ChainedProperty joinProperty) { + ChainedProperty newProperty = joinProperty.append(getChainedProperty()); + return getCanonical(newProperty, mSubFilter, mNot); + } + + @Override + NotJoined notJoinedFromCNF(ChainedProperty joinProperty) { + ChainedProperty notJoinedProp = getChainedProperty(); + ChainedProperty jp = joinProperty; + + while (notJoinedProp.getPrimeProperty().equals(jp.getPrimeProperty())) { + notJoinedProp = notJoinedProp.tail(); + if (jp.getChainCount() == 0) { + jp = null; + break; + } + jp = jp.tail(); + } + + if (jp != null || notJoinedProp.equals(getChainedProperty())) { + return super.notJoinedFromCNF(joinProperty); + } + + ExistsFilter notJoinedFilter = getCanonical(notJoinedProp, mSubFilter, mNot); + + return new NotJoined(notJoinedFilter, getOpenFilter(getStorableType())); + } + + Filter buildDisjunctiveNormalForm() { + return this; + } + + Filter buildConjunctiveNormalForm() { + return this; + } + + boolean isDisjunctiveNormalForm() { + return true; + } + + boolean isConjunctiveNormalForm() { + return true; + } + + boolean isReduced() { + return true; + } + + void markReduced() { + } + + @Override + public int hashCode() { + int hash = mProperty.hashCode() * 31 + mSubFilter.hashCode(); + return mNot ? ~hash : hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof ExistsFilter) { + ExistsFilter other = (ExistsFilter) obj; + return getStorableType() == other.getStorableType() + && mSubFilter == other.mSubFilter + && mNot == other.mNot + && mProperty.equals(other.mProperty); + } + return false; + } + + public void appendTo(Appendable app, FilterValues values) throws IOException { + if (mNot) { + app.append('!'); + } + mProperty.appendTo(app); + app.append('('); + + Filter subFilter = mSubFilter; + if (subFilter != null && !(subFilter.isOpen())) { + FilterValues subValues; + if (values == null) { + subValues = null; + } else { + FilterValues subInitialValues = mSubFilter.initialFilterValues(); + if (subInitialValues == null) { + subValues = null; + } else { + subValues = subInitialValues + .withValues(values.getSuppliedValuesFor(getJoinedSubFilter())); + subFilter = subValues.getFilter(); + } + } + subFilter.appendTo(app, subValues); + } + + app.append(')'); + } +} diff --git a/src/main/java/com/amazon/carbonado/filter/Filter.java b/src/main/java/com/amazon/carbonado/filter/Filter.java index cbc37dc..11f36d9 100644 --- a/src/main/java/com/amazon/carbonado/filter/Filter.java +++ b/src/main/java/com/amazon/carbonado/filter/Filter.java @@ -51,10 +51,12 @@ import com.amazon.carbonado.util.Appender; * AndFilter = NotFilter { "&" NotFilter } * NotFilter = [ "!" ] EntityFilter * EntityFilter = PropertyFilter + * = ChainedFilter * | "(" Filter ")" * PropertyFilter = ChainedProperty RelOp "?" * RelOp = "=" | "!=" | "<" | ">=" | ">" | "<=" * ChainedProperty = Identifier { "." Identifier } + * ChainedFilter = ChainedProperty "(" [ Filter ] ")" * * * @author Brian S O'Neill @@ -176,17 +178,12 @@ public abstract class Filter implements Appender { * difference is caused by the filter property values being {@link #bind bound}. */ public FilterValues initialFilterValues() { - if (mFilterValues == null) { - Filter boundFilter = bind(); - - if (boundFilter != this) { - return boundFilter.initialFilterValues(); - } - + FilterValues filterValues = mFilterValues; + if (filterValues == null) { buildFilterValues(); + filterValues = mFilterValues; } - - return mFilterValues; + return filterValues; } /** @@ -196,27 +193,33 @@ public abstract class Filter implements Appender { * @return tail of PropertyFilterList, or null if no parameters */ PropertyFilterList getTailPropertyFilterList() { - if (mTailPropertyFilterList == null) { + PropertyFilterList tail = mTailPropertyFilterList; + if (tail == null) { buildFilterValues(); + tail = mTailPropertyFilterList; } - - return mTailPropertyFilterList; + return tail; } private void buildFilterValues() { - PropertyFilterList list = accept(new PropertyFilterList.Builder(), null); + Filter boundFilter = bind(); + + if (boundFilter != this) { + mFilterValues = boundFilter.initialFilterValues(); + mTailPropertyFilterList = boundFilter.getTailPropertyFilterList(); + return; + } - // List should never be null since only OpenFilter and ClosedFilter - // have no properties, and they override initialFilterValues and - // getTailPropertyFilterList. - assert(list != null); + PropertyFilterList list = accept(new PropertyFilterList.Builder(), null); - // Since FilterValues instances are immutable, save this for re-use. - mFilterValues = FilterValues.create(this, list); + if (list != null) { + // Since FilterValues instances are immutable, save this for re-use. + mFilterValues = FilterValues.create(this, list); - // PropertyFilterList can be saved for re-use because it too is - // immutable (after PropertyFilterListBuilder has run). - mTailPropertyFilterList = list.get(-1); + // PropertyFilterList can be saved for re-use because it too is + // immutable (after PropertyFilterListBuilder has run). + mTailPropertyFilterList = list.get(-1); + } } /** @@ -239,10 +242,10 @@ public abstract class Filter implements Appender { * @throws IllegalArgumentException if filter is null */ public Filter and(Filter filter) { - if (filter instanceof OpenFilter) { + if (filter.isOpen()) { return this; } - if (filter instanceof ClosedFilter) { + if (filter.isClosed()) { return filter; } return AndFilter.getCanonical(this, filter); @@ -277,6 +280,38 @@ public abstract class Filter implements Appender { return and(PropertyFilter.getCanonical(prop, operator, 0).constant(constantValue)); } + /** + * Returns a combined filter instance that accepts records which are only + * accepted by this filter and the "exists" test applied to a one-to-many join. + * + * @param propertyName one-to-many join property name, which may be a chained property + * @param subFilter sub-filter to apply to one-to-many join, which may be + * null to test for any existing + * @return canonical Filter instance + * @throws IllegalArgumentException if property is not found + * @since 1.2 + */ + public final Filter andExists(String propertyName, Filter subFilter) { + ChainedProperty prop = new FilterParser(mType, propertyName).parseChainedProperty(); + return and(ExistsFilter.getCanonical(prop, subFilter, false)); + } + + /** + * Returns a combined filter instance that accepts records which are only + * accepted by this filter and the "not exists" test applied to a one-to-many join. + * + * @param propertyName one-to-many join property name, which may be a chained property + * @param subFilter sub-filter to apply to one-to-many join, which may be + * null to test for any not existing + * @return canonical Filter instance + * @throws IllegalArgumentException if property is not found + * @since 1.2 + */ + public final Filter andNotExists(String propertyName, Filter subFilter) { + ChainedProperty prop = new FilterParser(mType, propertyName).parseChainedProperty(); + return and(ExistsFilter.getCanonical(prop, subFilter, true)); + } + /** * Returns a combined filter instance that accepts records which are * accepted either by this filter or the one given. @@ -297,10 +332,10 @@ public abstract class Filter implements Appender { * @throws IllegalArgumentException if filter is null */ public Filter or(Filter filter) { - if (filter instanceof OpenFilter) { + if (filter.isOpen()) { return filter; } - if (filter instanceof ClosedFilter) { + if (filter.isClosed()) { return this; } return OrFilter.getCanonical(this, filter); @@ -335,6 +370,40 @@ public abstract class Filter implements Appender { return or(PropertyFilter.getCanonical(prop, operator, 0).constant(constantValue)); } + /** + * Returns a combined filter instance that accepts records which are + * accepted either by this filter or the "exists" test applied to a + * one-to-many join. + * + * @param propertyName one-to-many join property name, which may be a chained property + * @param subFilter sub-filter to apply to one-to-many join, which may be + * null to test for any existing + * @return canonical Filter instance + * @throws IllegalArgumentException if property is not found + * @since 1.2 + */ + public final Filter orExists(String propertyName, Filter subFilter) { + ChainedProperty prop = new FilterParser(mType, propertyName).parseChainedProperty(); + return or(ExistsFilter.getCanonical(prop, subFilter, false)); + } + + /** + * Returns a combined filter instance that accepts records which are + * accepted either by this filter or the "not exists" test applied to a + * one-to-many join. + * + * @param propertyName one-to-many join property name, which may be a chained property + * @param subFilter sub-filter to apply to one-to-many join, which may be + * null to test for any not existing + * @return canonical Filter instance + * @throws IllegalArgumentException if property is not found + * @since 1.2 + */ + public final Filter orNotExists(String propertyName, Filter subFilter) { + ChainedProperty prop = new FilterParser(mType, propertyName).parseChainedProperty(); + return or(ExistsFilter.getCanonical(prop, subFilter, true)); + } + /** * Returns the logical negation of this filter. * @@ -385,6 +454,11 @@ public abstract class Filter implements Appender { list.add(filter); return null; } + + public Object visit(ExistsFilter filter, Object param) { + list.add(filter); + return null; + } }, null); return Collections.unmodifiableList(list); @@ -441,6 +515,11 @@ public abstract class Filter implements Appender { list.add(filter); return null; } + + public Object visit(ExistsFilter filter, Object param) { + list.add(filter); + return null; + } }, null); return Collections.unmodifiableList(list); @@ -534,7 +613,19 @@ public abstract class Filter implements Appender { * @return filter for type T * @throws IllegalArgumentException if property is not a join to type S */ - public abstract Filter asJoinedFrom(ChainedProperty joinProperty); + public final Filter asJoinedFrom(ChainedProperty joinProperty) { + if (joinProperty.getType() != getStorableType()) { + throw new IllegalArgumentException + ("Property is not of type \"" + getStorableType().getName() + "\": " + + joinProperty); + } + return asJoinedFromAny(joinProperty); + } + + /** + * Allows join from any property type, including one-to-many joins. + */ + abstract Filter asJoinedFromAny(ChainedProperty joinProperty); /** * Removes a join property prefix from all applicable properties of this @@ -577,16 +668,17 @@ public abstract class Filter implements Appender { * @throws IllegalArgumentException if property does not refer to a Storable */ public final NotJoined notJoinedFrom(ChainedProperty joinProperty) { - Class type = joinProperty.getType(); - if (!Storable.class.isAssignableFrom(type)) { + if (!Storable.class.isAssignableFrom(joinProperty.getType())) { throw new IllegalArgumentException ("Join property type is not a Storable: " + joinProperty); } + return notJoinedFromAny(joinProperty); + } - Filter cnf = conjunctiveNormalForm(); - NotJoined nj = cnf.notJoinedFrom(joinProperty, (Class) type); + final NotJoined notJoinedFromAny(ChainedProperty joinProperty) { + NotJoined nj = conjunctiveNormalForm().notJoinedFromCNF(joinProperty); - if (nj.getNotJoinedFilter() instanceof OpenFilter) { + if (nj.getNotJoinedFilter().isOpen()) { // Remainder filter should be same as original, but it might have // expanded with conjunctive normal form. If so, restore to // original, but still bind it to ensure consistent side-effects. @@ -615,10 +707,26 @@ public abstract class Filter implements Appender { /** * Should only be called on a filter in conjunctive normal form. */ - NotJoined notJoinedFrom(ChainedProperty joinProperty, - Class joinPropertyType) - { - return new NotJoined(getOpenFilter(joinPropertyType), this); + NotJoined notJoinedFromCNF(ChainedProperty joinProperty) { + return new NotJoined(getOpenFilter(joinProperty.getLastProperty().getJoinedType()), this); + } + + /** + * Returns true if filter allows all results to pass through. + * + * @since 1.2 + */ + public boolean isOpen() { + return false; + } + + /** + * Returns true if filter prevents any results from passing through. + * + * @since 1.2 + */ + public boolean isClosed() { + return false; } abstract Filter buildDisjunctiveNormalForm(); diff --git a/src/main/java/com/amazon/carbonado/filter/FilterParser.java b/src/main/java/com/amazon/carbonado/filter/FilterParser.java index a0a178d..bb7dac8 100644 --- a/src/main/java/com/amazon/carbonado/filter/FilterParser.java +++ b/src/main/java/com/amazon/carbonado/filter/FilterParser.java @@ -41,6 +41,10 @@ class FilterParser { private int mPos; FilterParser(Class type, String filter) { + this(type, filter, 0); + } + + private FilterParser(Class type, String filter, int pos) { if (type == null) { throw new IllegalArgumentException(); } @@ -49,6 +53,7 @@ class FilterParser { } mType = type; mFilter = filter; + mPos = pos; } // Design note: This parser is actually a scanner, parser, and type checker @@ -120,12 +125,46 @@ class FilterParser { return test; } else { mPos--; - return parsePropertyFilter(); + ChainedProperty chained = parseChainedProperty(); + c = nextCharIgnoreWhitespace(); + if (c != '(') { + mPos--; + return parsePropertyFilter(chained); + } + + boolean isExistsFilter = chained.getLastProperty().isQuery(); + + Filter chainedFilter; + c = nextCharIgnoreWhitespace(); + if (c == ')') { + if (isExistsFilter) { + chainedFilter = ExistsFilter.getCanonical(chained, null, false); + } else { + // FIXME: support exists filter for this case + mPos--; + throw error("Property \"" + chained + + "\" is a many-to-one join and requires property filters"); + } + } else { + mPos--; + Filter cf = parseChainedFilter(chained); + if (isExistsFilter) { + chainedFilter = ExistsFilter.getCanonical(chained, cf, false); + } else { + chainedFilter = cf.asJoinedFrom(chained); + } + c = nextCharIgnoreWhitespace(); + if (c != ')') { + mPos--; + throw error("Right paren expected"); + } + } + + return chainedFilter; } } - - private PropertyFilter parsePropertyFilter() { - ChainedProperty chained = parseChainedProperty(); + + private PropertyFilter parsePropertyFilter(ChainedProperty chained) { int c = nextCharIgnoreWhitespace(); RelOp op; @@ -203,6 +242,15 @@ class FilterParser { } } + @SuppressWarnings("unchecked") + private Filter parseChainedFilter(ChainedProperty chained) { + FilterParser chainedParser = new FilterParser + (chained.getLastProperty().getJoinedType(), mFilter, mPos); + Filter chainedFilter = chainedParser.parseFilter(); + mPos = chainedParser.mPos; + return chainedFilter; + } + @SuppressWarnings("unchecked") ChainedProperty parseChainedProperty() { String ident = parseIdentifier(); @@ -246,7 +294,8 @@ class FilterParser { } } - return ChainedProperty.get(prime, (StorableProperty[]) chain.toArray(new StorableProperty[chain.size()])); + return ChainedProperty + .get(prime, (StorableProperty[]) chain.toArray(new StorableProperty[chain.size()])); } private String parseIdentifier() { diff --git a/src/main/java/com/amazon/carbonado/filter/Group.java b/src/main/java/com/amazon/carbonado/filter/Group.java index 6805bf4..1138567 100644 --- a/src/main/java/com/amazon/carbonado/filter/Group.java +++ b/src/main/java/com/amazon/carbonado/filter/Group.java @@ -130,5 +130,13 @@ class Group { public Boolean visit(PropertyFilter filter, Filter child) { return filter == child; } + + /** + * @return TRUE if overlap was found + */ + @Override + public Boolean visit(ExistsFilter filter, Filter child) { + return filter == child; + } } } diff --git a/src/main/java/com/amazon/carbonado/filter/OpenFilter.java b/src/main/java/com/amazon/carbonado/filter/OpenFilter.java index 7604a16..35659e6 100644 --- a/src/main/java/com/amazon/carbonado/filter/OpenFilter.java +++ b/src/main/java/com/amazon/carbonado/filter/OpenFilter.java @@ -36,6 +36,16 @@ public class OpenFilter extends Filter { super(type); } + /** + * Always returns true. + * + * @since 1.2 + */ + @Override + public final boolean isOpen() { + return true; + } + public Filter and(Filter filter) { return filter; } @@ -92,7 +102,7 @@ public class OpenFilter extends Filter { return true; } - public OpenFilter asJoinedFrom(ChainedProperty joinProperty) { + OpenFilter asJoinedFromAny(ChainedProperty joinProperty) { return getOpenFilter(joinProperty.getPrimeProperty().getEnclosingType()); } diff --git a/src/main/java/com/amazon/carbonado/filter/OrFilter.java b/src/main/java/com/amazon/carbonado/filter/OrFilter.java index d5748aa..a06912a 100644 --- a/src/main/java/com/amazon/carbonado/filter/OrFilter.java +++ b/src/main/java/com/amazon/carbonado/filter/OrFilter.java @@ -62,16 +62,14 @@ public class OrFilter extends BinaryOpFilter { return mLeft.unbind().or(mRight.unbind()); } - public Filter asJoinedFrom(ChainedProperty joinProperty) { - return mLeft.asJoinedFrom(joinProperty).or(mRight.asJoinedFrom(joinProperty)); + Filter asJoinedFromAny(ChainedProperty joinProperty) { + return mLeft.asJoinedFromAny(joinProperty).or(mRight.asJoinedFromAny(joinProperty)); } @Override - NotJoined notJoinedFrom(ChainedProperty joinProperty, - Class joinPropertyType) - { - NotJoined left = mLeft.notJoinedFrom(joinProperty, joinPropertyType); - NotJoined right = mRight.notJoinedFrom(joinProperty, joinPropertyType); + NotJoined notJoinedFromCNF(ChainedProperty joinProperty) { + NotJoined left = mLeft.notJoinedFromCNF(joinProperty); + NotJoined right = mRight.notJoinedFromCNF(joinProperty); // Assert that our child nodes are only OrFilter or PropertyFilter. if (!isConjunctiveNormalForm()) { @@ -85,10 +83,8 @@ public class OrFilter extends BinaryOpFilter { // and remainder filters would need to logically or'd together to // reform the original filter, breaking the notJoinedFrom contract. - if (!(left.getRemainderFilter() instanceof OpenFilter) || - !(right.getRemainderFilter() instanceof OpenFilter)) - { - return super.notJoinedFrom(joinProperty, joinPropertyType); + if (!(left.getRemainderFilter().isOpen()) || !(right.getRemainderFilter().isOpen())) { + return super.notJoinedFromCNF(joinProperty); } // Remove wildcards to shut the compiler up. diff --git a/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java b/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java index 9f12095..97d508a 100644 --- a/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java +++ b/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java @@ -188,13 +188,7 @@ public class PropertyFilter extends Filter { return mBindID != 0; } - public PropertyFilter asJoinedFrom(ChainedProperty joinProperty) { - if (joinProperty.getType() != getStorableType()) { - throw new IllegalArgumentException - ("Property is not of type \"" + getStorableType().getName() + "\": " + - joinProperty); - } - + PropertyFilter asJoinedFromAny(ChainedProperty joinProperty) { ChainedProperty newProperty = joinProperty.append(getChainedProperty()); if (isConstant()) { @@ -205,9 +199,7 @@ public class PropertyFilter extends Filter { } @Override - NotJoined notJoinedFrom(ChainedProperty joinProperty, - Class joinPropertyType) - { + NotJoined notJoinedFromCNF(ChainedProperty joinProperty) { ChainedProperty notJoinedProp = getChainedProperty(); ChainedProperty jp = joinProperty; @@ -221,7 +213,7 @@ public class PropertyFilter extends Filter { } if (jp != null || notJoinedProp.equals(getChainedProperty())) { - return super.notJoinedFrom(joinProperty, joinPropertyType); + return super.notJoinedFromCNF(joinProperty); } PropertyFilter notJoinedFilter; diff --git a/src/main/java/com/amazon/carbonado/filter/PropertyFilterList.java b/src/main/java/com/amazon/carbonado/filter/PropertyFilterList.java index 447edef..1027c67 100644 --- a/src/main/java/com/amazon/carbonado/filter/PropertyFilterList.java +++ b/src/main/java/com/amazon/carbonado/filter/PropertyFilterList.java @@ -182,5 +182,22 @@ class PropertyFilterList { public PropertyFilterList visit(PropertyFilter filter, PropertyFilterList list) { return list == null ? new PropertyFilterList(filter, null) : list.prepend(filter); } + + public PropertyFilterList visit(ExistsFilter filter, PropertyFilterList list) { + PropertyFilterList subList = + filter.getJoinedSubFilter().getTailPropertyFilterList(); + + while (subList != null) { + PropertyFilter joinedFilter = subList.getPropertyFilter(); + if (list == null) { + list = new PropertyFilterList(joinedFilter, null); + } else { + list = list.prepend(joinedFilter); + } + subList = subList.getPrevious(); + } + + return list; + } } } diff --git a/src/main/java/com/amazon/carbonado/filter/Reducer.java b/src/main/java/com/amazon/carbonado/filter/Reducer.java index 6e2acc1..1c3d400 100644 --- a/src/main/java/com/amazon/carbonado/filter/Reducer.java +++ b/src/main/java/com/amazon/carbonado/filter/Reducer.java @@ -100,4 +100,15 @@ class Reducer extends Visitor, Group> { group.add(filter); return null; } + + /** + * @param filter candidate node to potentially replace + * @param group gathered children + * @return original candidate or replacement + */ + @Override + public Filter visit(ExistsFilter filter, Group group) { + group.add(filter); + return null; + } } diff --git a/src/main/java/com/amazon/carbonado/filter/Visitor.java b/src/main/java/com/amazon/carbonado/filter/Visitor.java index 77ca07f..2c9e318 100644 --- a/src/main/java/com/amazon/carbonado/filter/Visitor.java +++ b/src/main/java/com/amazon/carbonado/filter/Visitor.java @@ -45,6 +45,13 @@ public abstract class Visitor { return null; } + /** + * @since 1.2 + */ + public R visit(ExistsFilter filter, P param) { + return null; + } + public R visit(OpenFilter filter, P param) { return null; } diff --git a/src/main/java/com/amazon/carbonado/info/ChainedProperty.java b/src/main/java/com/amazon/carbonado/info/ChainedProperty.java index 7d21a49..575bcbf 100644 --- a/src/main/java/com/amazon/carbonado/info/ChainedProperty.java +++ b/src/main/java/com/amazon/carbonado/info/ChainedProperty.java @@ -336,8 +336,9 @@ public class ChainedProperty implements Appender { } /** - * Returns the chained property in a parseable form. The format is - * "name.subname.subsubname". + * Returns the chained property formatted as "name.subname.subsubname". + * This format is parseable only if the chain is composed of valid + * many-to-one joins. */ @Override public String toString() { @@ -354,8 +355,9 @@ public class ChainedProperty implements Appender { } /** - * Appends the chained property in a parseable form. The format is - * "name.subname.subsubname". + * Appends the chained property formatted as "name.subname.subsubname". + * This format is parseable only if the chain is composed of valid + * many-to-one joins. */ public void appendTo(Appendable app) throws IOException { app.append(mPrime.getName()); diff --git a/src/main/java/com/amazon/carbonado/qe/EmptyQuery.java b/src/main/java/com/amazon/carbonado/qe/EmptyQuery.java index 1a970e0..626b435 100644 --- a/src/main/java/com/amazon/carbonado/qe/EmptyQuery.java +++ b/src/main/java/com/amazon/carbonado/qe/EmptyQuery.java @@ -194,15 +194,14 @@ public final class EmptyQuery extends AbstractQuery { } public Query or(Filter filter) throws FetchException { - FilterValues values = filter == null ? null : filter.initialFilterValues(); - return mFactory.query(values, mOrdering); + return mFactory.query(filter, null, mOrdering); } /** * Returns a query that fetches everything, possibly in a specified order. */ public Query not() throws FetchException { - return mFactory.query(null, mOrdering); + return mFactory.query(null, null, mOrdering); } public Query orderBy(String property) throws FetchException { diff --git a/src/main/java/com/amazon/carbonado/qe/FilteredQueryExecutor.java b/src/main/java/com/amazon/carbonado/qe/FilteredQueryExecutor.java index d431b3d..494d353 100644 --- a/src/main/java/com/amazon/carbonado/qe/FilteredQueryExecutor.java +++ b/src/main/java/com/amazon/carbonado/qe/FilteredQueryExecutor.java @@ -26,10 +26,8 @@ import com.amazon.carbonado.Storable; import com.amazon.carbonado.cursor.FilteredCursor; -import com.amazon.carbonado.filter.ClosedFilter; import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.FilterValues; -import com.amazon.carbonado.filter.OpenFilter; /** * QueryExecutor which wraps another and filters results. @@ -50,12 +48,16 @@ public class FilteredQueryExecutor extends AbstractQueryExec if (executor == null) { throw new IllegalArgumentException(); } - if (filter == null || filter instanceof OpenFilter || filter instanceof ClosedFilter) { + if (filter == null || filter.isOpen() || filter.isClosed()) { throw new IllegalArgumentException(); } mExecutor = executor; // Ensure filter is same as what will be provided by values. - mFilter = filter.initialFilterValues().getFilter(); + FilterValues values = filter.initialFilterValues(); + if (values != null) { + filter = values.getFilter(); + } + mFilter = filter; } public Cursor fetch(FilterValues values) throws FetchException { diff --git a/src/main/java/com/amazon/carbonado/qe/JoinedQueryExecutor.java b/src/main/java/com/amazon/carbonado/qe/JoinedQueryExecutor.java index b1d47e9..12238a6 100644 --- a/src/main/java/com/amazon/carbonado/qe/JoinedQueryExecutor.java +++ b/src/main/java/com/amazon/carbonado/qe/JoinedQueryExecutor.java @@ -42,7 +42,6 @@ import com.amazon.carbonado.cursor.MultiTransformedCursor; import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.FilterValues; -import com.amazon.carbonado.filter.OpenFilter; import com.amazon.carbonado.filter.RelOp; import com.amazon.carbonado.info.ChainedProperty; @@ -501,7 +500,7 @@ public class JoinedQueryExecutor throw new IllegalArgumentException("Outer loop executor filter must be bound"); } - if (targetFilter instanceof OpenFilter) { + if (targetFilter.isOpen()) { targetFilter = null; } diff --git a/src/main/java/com/amazon/carbonado/qe/QueryEngine.java b/src/main/java/com/amazon/carbonado/qe/QueryEngine.java index 56f6145..8218b66 100644 --- a/src/main/java/com/amazon/carbonado/qe/QueryEngine.java +++ b/src/main/java/com/amazon/carbonado/qe/QueryEngine.java @@ -49,13 +49,20 @@ public class QueryEngine extends StandardQueryFactory return mExecutorFactory.executor(filter, ordering); } - protected StandardQuery createQuery(FilterValues values, OrderingList ordering) { - return new Query(values, ordering, null); + protected StandardQuery createQuery(Filter filter, + FilterValues values, + OrderingList ordering) + { + return new Query(filter, values, ordering, null); } private class Query extends StandardQuery { - Query(FilterValues values, OrderingList ordering, QueryExecutor executor) { - super(values, ordering, executor); + Query(Filter filter, + FilterValues values, + OrderingList ordering, + QueryExecutor executor) + { + super(filter, values, ordering, executor); } protected Transaction enterTransaction(IsolationLevel level) { @@ -74,7 +81,7 @@ public class QueryEngine extends StandardQueryFactory OrderingList ordering, QueryExecutor executor) { - return new Query(values, ordering, executor); + return new Query(values.getFilter(), values, ordering, executor); } } } diff --git a/src/main/java/com/amazon/carbonado/qe/QueryFactory.java b/src/main/java/com/amazon/carbonado/qe/QueryFactory.java index b7ead1d..ed1bfb4 100644 --- a/src/main/java/com/amazon/carbonado/qe/QueryFactory.java +++ b/src/main/java/com/amazon/carbonado/qe/QueryFactory.java @@ -22,6 +22,7 @@ import com.amazon.carbonado.FetchException; import com.amazon.carbonado.Query; import com.amazon.carbonado.Storable; +import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.FilterValues; /** @@ -35,8 +36,10 @@ public interface QueryFactory { /** * Returns a query that handles the given query specification. * - * @param values optional values object, defaults to open filter if null + * @param filter optional filter object, defaults to open filter if null + * @param values optional values object, defaults to filter initial values * @param ordering optional order-by properties */ - Query query(FilterValues values, OrderingList ordering) throws FetchException; + Query query(Filter filter, FilterValues values, OrderingList ordering) + throws FetchException; } diff --git a/src/main/java/com/amazon/carbonado/qe/StandardQuery.java b/src/main/java/com/amazon/carbonado/qe/StandardQuery.java index 2d7995e..2914bf8 100644 --- a/src/main/java/com/amazon/carbonado/qe/StandardQuery.java +++ b/src/main/java/com/amazon/carbonado/qe/StandardQuery.java @@ -32,10 +32,8 @@ import com.amazon.carbonado.Storable; import com.amazon.carbonado.Transaction; import com.amazon.carbonado.Query; -import com.amazon.carbonado.filter.ClosedFilter; import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.FilterValues; -import com.amazon.carbonado.filter.OpenFilter; import com.amazon.carbonado.filter.RelOp; import com.amazon.carbonado.info.Direction; @@ -51,6 +49,8 @@ import com.amazon.carbonado.util.Appender; public abstract class StandardQuery extends AbstractQuery implements Appender { + // Filter for this query, which may be null. + private final Filter mFilter; // Values for this query, which may be null. private final FilterValues mValues; // Properties that this query is ordered by. @@ -59,15 +59,29 @@ public abstract class StandardQuery extends AbstractQuery private volatile QueryExecutor mExecutor; /** - * @param values optional values object, defaults to open filter if null + * @param filter optional filter object, defaults to open filter if null + * @param values optional values object, defaults to filter initial values * @param ordering optional order-by properties * @param executor optional executor to use (by default lazily obtains and caches executor) */ - protected StandardQuery(FilterValues values, + protected StandardQuery(Filter filter, + FilterValues values, OrderingList ordering, QueryExecutor executor) { + if (filter != null && filter.isOpen()) { + filter = null; + } + + if (values == null) { + if (filter != null) { + values = filter.initialFilterValues(); + } + } + + mFilter = filter; mValues = values; + if (ordering == null) { ordering = OrderingList.emptyList(); } @@ -80,11 +94,11 @@ public abstract class StandardQuery extends AbstractQuery } public Filter getFilter() { - FilterValues values = mValues; - if (values != null) { - return values.getFilter(); + Filter filter = mFilter; + if (filter == null) { + return Filter.getOpenFilter(getStorableType()); } - return Filter.getOpenFilter(getStorableType()); + return filter; } public FilterValues getFilterValues() { @@ -139,46 +153,57 @@ public abstract class StandardQuery extends AbstractQuery } public Query and(Filter filter) throws FetchException { + Filter newFilter; FilterValues newValues; - if (mValues == null) { + if (mFilter == null) { + newFilter = filter; newValues = filter.initialFilterValues(); } else { if (getBlankParameterCount() > 0) { throw new IllegalStateException("Blank parameters exist in query: " + this); } - newValues = mValues.getFilter().and(filter) - .initialFilterValues().withValues(mValues.getSuppliedValues()); + newFilter = mFilter.and(filter); + newValues = newFilter.initialFilterValues(); + if (mValues != null) { + newValues = newValues.withValues(mValues.getSuppliedValues()); + } } - return createQuery(newValues, mOrdering); + return createQuery(newFilter, newValues, mOrdering); } public Query or(Filter filter) throws FetchException { - if (mValues == null) { + if (mFilter == null) { throw new IllegalStateException("Query is already guaranteed to fetch everything"); } if (getBlankParameterCount() > 0) { throw new IllegalStateException("Blank parameters exist in query: " + this); } - FilterValues newValues = mValues.getFilter().or(filter) - .initialFilterValues().withValues(mValues.getSuppliedValues()); - return createQuery(newValues, mOrdering); + Filter newFilter = mFilter.or(filter); + FilterValues newValues = newFilter.initialFilterValues(); + if (mValues != null) { + newValues = newValues.withValues(mValues.getSuppliedValues()); + } + return createQuery(newFilter, newValues, mOrdering); } public Query not() throws FetchException { - if (mValues == null) { + if (mFilter == null) { return new EmptyQuery(queryFactory(), mOrdering); } - FilterValues newValues = mValues.getFilter().not() - .initialFilterValues().withValues(mValues.getSuppliedValues()); - return createQuery(newValues, mOrdering); + Filter newFilter = mFilter.not(); + FilterValues newValues = newFilter.initialFilterValues(); + if (mValues != null) { + newValues = newValues.withValues(mValues.getSuppliedValues()); + } + return createQuery(newFilter, newValues, mOrdering); } public Query orderBy(String property) throws FetchException { - return createQuery(mValues, OrderingList.get(getStorableType(), property)); + return createQuery(mFilter, mValues, OrderingList.get(getStorableType(), property)); } public Query orderBy(String... properties) throws FetchException { - return createQuery(mValues, OrderingList.get(getStorableType(), properties)); + return createQuery(mFilter, mValues, OrderingList.get(getStorableType(), properties)); } public Cursor fetch() throws FetchException { @@ -323,6 +348,9 @@ public abstract class StandardQuery extends AbstractQuery public int hashCode() { int hash = queryFactory().hashCode(); hash = hash * 31 + executorFactory().hashCode(); + if (mFilter != null) { + hash = hash * 31 + mFilter.hashCode(); + } if (mValues != null) { hash = hash * 31 + mValues.hashCode(); } @@ -339,6 +367,7 @@ public abstract class StandardQuery extends AbstractQuery StandardQuery other = (StandardQuery) obj; return queryFactory().equals(other.queryFactory()) && executorFactory().equals(other.executorFactory()) + && (mFilter == null ? (other.mFilter == null) : (mFilter.equals(other.mFilter))) && (mValues == null ? (other.mValues == null) : (mValues.equals(other.mValues))) && mOrdering.equals(other.mOrdering); } @@ -350,7 +379,7 @@ public abstract class StandardQuery extends AbstractQuery app.append(getStorableType().getName()); app.append(", filter="); Filter filter = getFilter(); - if (filter instanceof OpenFilter || filter instanceof ClosedFilter) { + if (filter.isOpen() || filter.isClosed()) { filter.appendTo(app); } else { app.append('"'); @@ -386,8 +415,7 @@ public abstract class StandardQuery extends AbstractQuery protected QueryExecutor executor() throws RepositoryException { QueryExecutor executor = mExecutor; if (executor == null) { - Filter filter = mValues == null ? null : mValues.getFilter(); - mExecutor = executor = executorFactory().executor(filter, mOrdering); + mExecutor = executor = executorFactory().executor(mFilter, mOrdering); } return executor; } @@ -406,8 +434,7 @@ public abstract class StandardQuery extends AbstractQuery */ protected void resetExecutor() throws RepositoryException { if (mExecutor != null) { - Filter filter = mValues == null ? null : mValues.getFilter(); - mExecutor = executorFactory().executor(filter, mOrdering); + mExecutor = executorFactory().executor(mFilter, mOrdering); } } @@ -443,7 +470,7 @@ public abstract class StandardQuery extends AbstractQuery * new filter values. The Filter in the FilterValues is the same as was * passed in the constructor. * - * @param values optional values object, defaults to open filter if null + * @param values non-null values object * @param ordering order-by properties, never null */ protected abstract StandardQuery newInstance(FilterValues values, @@ -454,9 +481,11 @@ public abstract class StandardQuery extends AbstractQuery return newInstance(values, mOrdering, mExecutor); } - private Query createQuery(FilterValues values, OrderingList ordering) + private Query createQuery(Filter filter, + FilterValues values, + OrderingList ordering) throws FetchException { - return queryFactory().query(values, ordering); + return queryFactory().query(filter, values, ordering); } } diff --git a/src/main/java/com/amazon/carbonado/qe/StandardQueryFactory.java b/src/main/java/com/amazon/carbonado/qe/StandardQueryFactory.java index 06767c5..1ce509b 100644 --- a/src/main/java/com/amazon/carbonado/qe/StandardQueryFactory.java +++ b/src/main/java/com/amazon/carbonado/qe/StandardQueryFactory.java @@ -28,7 +28,6 @@ import com.amazon.carbonado.FetchException; import com.amazon.carbonado.Query; import com.amazon.carbonado.RepositoryException; import com.amazon.carbonado.Storable; -import com.amazon.carbonado.filter.ClosedFilter; import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.FilterValues; @@ -109,6 +108,8 @@ public abstract class StandardQueryFactory implements QueryF * @throws IllegalArgumentException if filter is null */ public Query query(Filter filter, OrderingList ordering) throws FetchException { + filter = filter.bind(); + Map, Query> map; synchronized (mFilterToQuery) { map = mFilterToQuery.get(filter); @@ -126,10 +127,10 @@ public abstract class StandardQueryFactory implements QueryF query = map.get(ordering); if (query == null) { FilterValues values = filter.initialFilterValues(); - if (values == null && filter instanceof ClosedFilter) { + if (values == null && filter.isClosed()) { query = new EmptyQuery(this, ordering); } else { - StandardQuery standardQuery = createQuery(values, ordering); + StandardQuery standardQuery = createQuery(filter, values, ordering); if (!mLazySetExecutor) { try { standardQuery.setExecutor(); @@ -149,16 +150,19 @@ public abstract class StandardQueryFactory implements QueryF /** * Returns a new or cached query for the given query specification. * - * @param values optional values object, defaults to open filter if null + * @param filter optional filter object, defaults to open filter if null + * @param values optional values object, defaults to filter initial values * @param ordering optional order-by properties */ - public Query query(FilterValues values, OrderingList ordering) throws FetchException { - Query query; - if (values == null) { - query = query(Filter.getOpenFilter(mType), ordering); - } else { - query = query(values.getFilter(), ordering).withValues(values.getSuppliedValues()); + public Query query(Filter filter, FilterValues values, OrderingList ordering) + throws FetchException + { + Query query = query(filter != null ? filter : Filter.getOpenFilter(mType), ordering); + + if (values != null) { + query = query.withValues(values.getSuppliedValues()); } + return query; } @@ -196,10 +200,12 @@ public abstract class StandardQueryFactory implements QueryF /** * Implement this method to return query implementations. * - * @param values optional values object, defaults to open filter if null + * @param filter optional filter object, defaults to open filter if null + * @param values optional values object, defaults to filter initial values * @param ordering optional order-by properties */ - protected abstract StandardQuery createQuery(FilterValues values, + protected abstract StandardQuery createQuery(Filter filter, + FilterValues values, OrderingList ordering) throws FetchException; diff --git a/src/main/java/com/amazon/carbonado/qe/UnionQueryAnalyzer.java b/src/main/java/com/amazon/carbonado/qe/UnionQueryAnalyzer.java index fda5f29..1771c71 100644 --- a/src/main/java/com/amazon/carbonado/qe/UnionQueryAnalyzer.java +++ b/src/main/java/com/amazon/carbonado/qe/UnionQueryAnalyzer.java @@ -33,6 +33,7 @@ import com.amazon.carbonado.Storable; import com.amazon.carbonado.SupportException; import com.amazon.carbonado.filter.AndFilter; +import com.amazon.carbonado.filter.ExistsFilter; import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.OrFilter; import com.amazon.carbonado.filter.PropertyFilter; @@ -625,6 +626,17 @@ public class UnionQueryAnalyzer implements QueryExecutorFact } } + // This method should only be called if root filter has no logical operators. + @Override + public RepositoryException visit(ExistsFilter filter, Object param) { + try { + subAnalyze(filter); + return null; + } catch (RepositoryException e) { + return e; + } + } + private void subAnalyze(Filter subFilter) throws SupportException, RepositoryException { IndexedQueryAnalyzer.Result subResult = mIndexAnalyzer.analyze(subFilter, mOrdering); diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorage.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorage.java index 25b0209..c0f23a2 100644 --- a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorage.java +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorage.java @@ -45,6 +45,7 @@ import com.amazon.carbonado.Transaction; import com.amazon.carbonado.Trigger; import com.amazon.carbonado.capability.IndexInfo; import com.amazon.carbonado.filter.AndFilter; +import com.amazon.carbonado.filter.ExistsFilter; import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.FilterValues; import com.amazon.carbonado.filter.OrFilter; @@ -287,8 +288,11 @@ class JDBCStorage extends StandardQueryFactory return mInfo; } - protected StandardQuery createQuery(FilterValues values, OrderingList ordering) { - return new JDBCQuery(values, ordering, null); + protected StandardQuery createQuery(Filter filter, + FilterValues values, + OrderingList ordering) + { + return new JDBCQuery(filter, values, ordering, null); } public S instantiate(ResultSet rs) throws SQLException { @@ -313,24 +317,23 @@ class JDBCStorage extends StandardQueryFactory JoinNode jn; try { - JoinNodeBuilder jnb = new JoinNodeBuilder(aliasGenerator); - if (filter == null) { - jn = new JoinNode(getStorableInfo(), null); - } else { + JoinNodeBuilder jnb = + new JoinNodeBuilder(mRepository, getStorableInfo(), aliasGenerator); + if (filter != null) { filter.accept(jnb, null); - jn = jnb.getRootJoinNode(); } + jn = jnb.getRootJoinNode(); jnb.captureOrderings(ordering); } catch (UndeclaredThrowableException e) { throw mRepository.toFetchException(e); } - StatementBuilder selectBuilder = new StatementBuilder(); + StatementBuilder selectBuilder = new StatementBuilder(mRepository); selectBuilder.append("SELECT "); // Don't bother using a table alias for one table. With just one table, // there's no need to disambiguate. - String alias = jn.hasAnyJoins() ? jn.getAlias() : null; + String alias = jn.isAliasRequired() ? jn.getAlias() : null; Map> properties = getStorableInfo().getAllProperties(); int ordinal = 0; @@ -351,7 +354,7 @@ class JDBCStorage extends StandardQueryFactory selectBuilder.append(" FROM"); - StatementBuilder fromWhereBuilder = new StatementBuilder(); + StatementBuilder fromWhereBuilder = new StatementBuilder(mRepository); fromWhereBuilder.append(" FROM"); if (alias == null) { @@ -366,7 +369,7 @@ class JDBCStorage extends StandardQueryFactory PropertyFilter[] propertyFilters; boolean[] propertyFilterNullable; - if (filter == null) { + if (filter == null || filter.isOpen()) { propertyFilters = null; propertyFilterNullable = null; } else { @@ -374,7 +377,8 @@ class JDBCStorage extends StandardQueryFactory selectBuilder.append(" WHERE "); fromWhereBuilder.append(" WHERE "); - WhereBuilder wb = new WhereBuilder(selectBuilder, alias == null ? null : jn); + WhereBuilder wb = new WhereBuilder + (selectBuilder, alias == null ? null : jn, aliasGenerator); FetchException e = filter.accept(wb, null); if (e != null) { throw e; @@ -383,7 +387,8 @@ class JDBCStorage extends StandardQueryFactory propertyFilters = wb.getPropertyFilters(); propertyFilterNullable = wb.getPropertyFilterNullable(); - wb = new WhereBuilder(fromWhereBuilder, alias == null ? null : jn); + wb = new WhereBuilder + (fromWhereBuilder, alias == null ? null : jn, aliasGenerator); e = filter.accept(wb, null); if (e != null) { throw e; @@ -686,8 +691,12 @@ class JDBCStorage extends StandardQueryFactory } private class JDBCQuery extends StandardQuery { - JDBCQuery(FilterValues values, OrderingList ordering, QueryExecutor executor) { - super(values, ordering, executor); + JDBCQuery(Filter filter, + FilterValues values, + OrderingList ordering, + QueryExecutor executor) + { + super(filter, values, ordering, executor); } @Override @@ -721,14 +730,14 @@ class JDBCStorage extends StandardQueryFactory OrderingList ordering, QueryExecutor executor) { - return new JDBCQuery(values, ordering, executor); + return new JDBCQuery(values.getFilter(), values, ordering, executor); } } /** * Node in a tree structure describing how tables are joined together. */ - private class JoinNode { + private static class JoinNode { // Joined property which led to this node. For root node, it is null. private final JDBCStorableProperty mProperty; @@ -737,6 +746,8 @@ class JDBCStorage extends StandardQueryFactory private final Map mSubNodes; + private boolean mAliasRequired; + /** * @param alias table alias in SQL statement, i.e. "T1" */ @@ -782,8 +793,8 @@ class JDBCStorage extends StandardQueryFactory return null; } - public boolean hasAnyJoins() { - return mSubNodes.size() > 0; + public boolean isAliasRequired() { + return mAliasRequired || mSubNodes.size() > 0; } /** @@ -843,13 +854,16 @@ class JDBCStorage extends StandardQueryFactory } } - public void addJoin(ChainedProperty chained, TableAliasGenerator aliasGenerator) + public void addJoin(JDBCRepository repository, + ChainedProperty chained, + TableAliasGenerator aliasGenerator) throws RepositoryException { - addJoin(chained, aliasGenerator, 0); + addJoin(repository, chained, aliasGenerator, 0); } - private void addJoin(ChainedProperty chained, + private void addJoin(JDBCRepository repository, + ChainedProperty chained, TableAliasGenerator aliasGenerator, int offset) throws RepositoryException @@ -867,12 +881,16 @@ class JDBCStorage extends StandardQueryFactory String name = property.getName(); JoinNode subNode = mSubNodes.get(name); if (subNode == null) { - JDBCStorableInfo info = mRepository.examineStorable(property.getJoinedType()); - JDBCStorableProperty jProperty = mRepository.getJDBCStorableProperty(property); + JDBCStorableInfo info = repository.examineStorable(property.getJoinedType()); + JDBCStorableProperty jProperty = repository.getJDBCStorableProperty(property); subNode = new JoinNode(jProperty, info, aliasGenerator.nextAlias()); mSubNodes.put(name, subNode); } - subNode.addJoin(chained, aliasGenerator, offset + 1); + subNode.addJoin(repository, chained, aliasGenerator, offset + 1); + } + + public void aliasIsRequired() { + mAliasRequired = true; } public String toString() { @@ -893,13 +911,18 @@ class JDBCStorage extends StandardQueryFactory /** * Filter visitor that constructs a JoinNode tree. */ - private class JoinNodeBuilder extends Visitor { + private static class JoinNodeBuilder extends Visitor { + private final JDBCRepository mRepository; private final TableAliasGenerator mAliasGenerator; private final JoinNode mRootJoinNode; - JoinNodeBuilder(TableAliasGenerator aliasGenerator) { + JoinNodeBuilder(JDBCRepository repository, + JDBCStorableInfo info, + TableAliasGenerator aliasGenerator) + { + mRepository = repository; mAliasGenerator = aliasGenerator; - mRootJoinNode = new JoinNode(getStorableInfo(), aliasGenerator.nextAlias()); + mRootJoinNode = new JoinNode(info, aliasGenerator.nextAlias()); } public JoinNode getRootJoinNode() { @@ -917,7 +940,7 @@ class JDBCStorage extends StandardQueryFactory if (ordering != null) { for (OrderedProperty orderedProperty : ordering) { ChainedProperty chained = orderedProperty.getChainedProperty(); - mRootJoinNode.addJoin(chained, mAliasGenerator); + mRootJoinNode.addJoin(mRepository, chained, mAliasGenerator); } } } catch (RepositoryException e) { @@ -940,15 +963,37 @@ class JDBCStorage extends StandardQueryFactory private void visit(PropertyFilter filter) throws RepositoryException { ChainedProperty chained = filter.getChainedProperty(); - mRootJoinNode.addJoin(chained, mAliasGenerator); + mRootJoinNode.addJoin(mRepository, chained, mAliasGenerator); + } + + /** + * @throws UndeclaredThrowableException wraps a RepositoryException + * since RepositoryException cannot be thrown directly + */ + public Object visit(ExistsFilter filter, Object param) { + try { + visit(filter); + return null; + } catch (RepositoryException e) { + throw new UndeclaredThrowableException(e); + } + } + + private void visit(ExistsFilter filter) throws RepositoryException { + mRootJoinNode.aliasIsRequired(); + ChainedProperty chained = filter.getChainedProperty(); + mRootJoinNode.addJoin(mRepository, chained, mAliasGenerator); } } - private class StatementBuilder { + private static class StatementBuilder { + private final JDBCRepository mRepository; + private List> mStatements; private StringBuilder mLiteralBuilder; - StatementBuilder() { + StatementBuilder(JDBCRepository repository) { + mRepository = repository; mStatements = new ArrayList>(); mLiteralBuilder = new StringBuilder(); } @@ -1013,18 +1058,31 @@ class JDBCStorage extends StandardQueryFactory } mLiteralBuilder.append(jProperty.getColumnName()); } + + JDBCRepository getRepository() { + return mRepository; + } } - private class WhereBuilder extends Visitor { + private static class WhereBuilder + extends Visitor + { private final StatementBuilder mStatementBuilder; private final JoinNode mJoinNode; + private final TableAliasGenerator mAliasGenerator; private List> mPropertyFilters; private List mPropertyFilterNullable; - WhereBuilder(StatementBuilder statementBuilder, JoinNode jn) { + /** + * @param aliasGenerator used for supporting "EXISTS" filter + */ + WhereBuilder(StatementBuilder statementBuilder, JoinNode jn, + TableAliasGenerator aliasGenerator) + { mStatementBuilder = statementBuilder; mJoinNode = jn; + mAliasGenerator = aliasGenerator; mPropertyFilters = new ArrayList>(); mPropertyFilterNullable = new ArrayList(); } @@ -1114,6 +1172,74 @@ class JDBCStorage extends StandardQueryFactory return null; } + public FetchException visit(ExistsFilter filter, Object param) { + if (filter.isNotExists()) { + mStatementBuilder.append("NOT "); + } + mStatementBuilder.append("EXISTS (SELECT * FROM"); + + ChainedProperty chained = filter.getChainedProperty(); + + JDBCStorableInfo oneToManyInfo; + JDBCStorableProperty oneToMany; + + final JDBCRepository repo = mStatementBuilder.getRepository(); + try { + StorableProperty lastProp = chained.getLastProperty(); + oneToManyInfo = repo.examineStorable(lastProp.getJoinedType()); + oneToMany = repo.getJDBCStorableProperty(lastProp); + } catch (RepositoryException e) { + return repo.toFetchException(e); + } + + Filter subFilter = filter.getSubFilter(); + + JoinNode oneToManyNode; + try { + JoinNodeBuilder jnb = + new JoinNodeBuilder(repo, oneToManyInfo, mAliasGenerator); + if (subFilter != null) { + subFilter.accept(jnb, null); + } + oneToManyNode = jnb.getRootJoinNode(); + } catch (UndeclaredThrowableException e) { + return repo.toFetchException(e); + } + + oneToManyNode.appendFullJoinTo(mStatementBuilder); + + mStatementBuilder.append(" WHERE "); + + int count = oneToMany.getJoinElementCount(); + for (int i=0; i 0) { + mStatementBuilder.append(" AND "); + } + mStatementBuilder.append(oneToManyNode.getAlias()); + mStatementBuilder.append('.'); + mStatementBuilder.append(oneToMany.getInternalJoinElement(i).getColumnName()); + mStatementBuilder.append('='); + mStatementBuilder.append(mJoinNode.findAliasFor(chained)); + mStatementBuilder.append('.'); + mStatementBuilder.append(oneToMany.getExternalJoinElement(i).getColumnName()); + } + + if (subFilter != null && !subFilter.isOpen()) { + mStatementBuilder.append(" AND ("); + WhereBuilder wb = new WhereBuilder + (mStatementBuilder, oneToManyNode, mAliasGenerator); + FetchException e = (FetchException) subFilter.accept(wb, null); + if (e != null) { + return e; + } + mStatementBuilder.append(')'); + } + + mStatementBuilder.append(')'); + + return null; + } + private void addBindParameter(PropertyFilter filter) { RelOp op = filter.getOperator(); StorableProperty property = filter.getChainedProperty().getLastProperty(); -- cgit v1.2.3