summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian S. O'Neill <bronee@gmail.com>2007-10-26 21:24:43 +0000
committerBrian S. O'Neill <bronee@gmail.com>2007-10-26 21:24:43 +0000
commit4063328f97c0180ceab565cc3f411e3dcc07bca8 (patch)
treea4a9273fc849990dd1e02f8fe3f097b2e68cea01
parentf1393c44e8e0f30da15a3443ebbf0c022c608fca (diff)
Added support for outer joins.
-rw-r--r--RELEASE-NOTES.txt1
-rw-r--r--src/main/java/com/amazon/carbonado/Storage.java6
-rw-r--r--src/main/java/com/amazon/carbonado/cursor/FilteredCursorGenerator.java11
-rw-r--r--src/main/java/com/amazon/carbonado/filter/Filter.java6
-rw-r--r--src/main/java/com/amazon/carbonado/filter/FilterParser.java127
-rw-r--r--src/main/java/com/amazon/carbonado/filter/PropertyFilter.java34
-rw-r--r--src/main/java/com/amazon/carbonado/info/ChainedProperty.java234
-rw-r--r--src/main/java/com/amazon/carbonado/qe/IndexedQueryAnalyzer.java8
-rw-r--r--src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorage.java74
9 files changed, 430 insertions, 71 deletions
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 60880db..c844408 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -41,6 +41,7 @@ Carbonado change history
which resolve to null are cached as before, but non-nullable join properties
do not cache null.
- Added support for "where exists" in queries via new syntax.
+- Added support for outer joins via new syntax.
1.1 to 1.1.2
-------------------------------
diff --git a/src/main/java/com/amazon/carbonado/Storage.java b/src/main/java/com/amazon/carbonado/Storage.java
index 551e1eb..a1cc420 100644
--- a/src/main/java/com/amazon/carbonado/Storage.java
+++ b/src/main/java/com/amazon/carbonado/Storage.java
@@ -90,8 +90,12 @@ public interface Storage<S extends Storable> {
* | "(" Filter ")"
* PropertyFilter = ChainedProperty RelOp "?"
* RelOp = "=" | "!=" | "&lt;" | "&gt;=" | "&gt;" | "&lt;="
- * ChainedProperty = Identifier { "." Identifier }
* ChainedFilter = ChainedProperty "(" [ Filter ] ")"
+ * ChainedProperty = Identifier
+ * | InnerJoin "." ChainedProperty
+ * | OuterJoin "." ChainedProperty
+ * InnerJoin = Identifier
+ * OuterJoin = '(' Identifier ')'
* </pre>
*
* @param filter query filter expression
diff --git a/src/main/java/com/amazon/carbonado/cursor/FilteredCursorGenerator.java b/src/main/java/com/amazon/carbonado/cursor/FilteredCursorGenerator.java
index 04ea2e8..dba2062 100644
--- a/src/main/java/com/amazon/carbonado/cursor/FilteredCursorGenerator.java
+++ b/src/main/java/com/amazon/carbonado/cursor/FilteredCursorGenerator.java
@@ -464,18 +464,23 @@ class FilteredCursorGenerator {
/**
* Generated code checks if chained properties resolve to null, and if
- * so, branches to the current scope's fail location.
+ * so, branches to the current scope's fail or success location.
*/
private void loadChainedProperty(CodeBuilder b, ChainedProperty<?> chained) {
b.loadLocal(mStorableVar);
loadProperty(b, chained.getPrimeProperty());
for (int i=0; i<chained.getChainCount(); i++) {
- // Check if last loaded property was null, and fail if so.
+ // Check if last loaded property was null. Fail for inner join,
+ // success for outer join.
b.dup();
Label notNull = b.createLabel();
b.ifNullBranch(notNull, false);
b.pop();
- getScope().fail(b);
+ if (chained.isOuterJoin(i)) {
+ getScope().success(b);
+ } else {
+ getScope().fail(b);
+ }
notNull.setLocation();
// Now load next property in chain.
diff --git a/src/main/java/com/amazon/carbonado/filter/Filter.java b/src/main/java/com/amazon/carbonado/filter/Filter.java
index 4f32179..be70ff1 100644
--- a/src/main/java/com/amazon/carbonado/filter/Filter.java
+++ b/src/main/java/com/amazon/carbonado/filter/Filter.java
@@ -55,8 +55,12 @@ import com.amazon.carbonado.util.Appender;
* | "(" Filter ")"
* PropertyFilter = ChainedProperty RelOp "?"
* RelOp = "=" | "!=" | "&lt;" | "&gt;=" | "&gt;" | "&lt;="
- * ChainedProperty = Identifier { "." Identifier }
* ChainedFilter = ChainedProperty "(" [ Filter ] ")"
+ * ChainedProperty = Identifier
+ * | InnerJoin "." ChainedProperty
+ * | OuterJoin "." ChainedProperty
+ * InnerJoin = Identifier
+ * OuterJoin = '(' Identifier ')'
* </pre>
*
* @author Brian S O'Neill
diff --git a/src/main/java/com/amazon/carbonado/filter/FilterParser.java b/src/main/java/com/amazon/carbonado/filter/FilterParser.java
index 5d3762e..ed9cec6 100644
--- a/src/main/java/com/amazon/carbonado/filter/FilterParser.java
+++ b/src/main/java/com/amazon/carbonado/filter/FilterParser.java
@@ -115,7 +115,22 @@ class FilterParser<S extends Storable> {
private Filter<S> parseEntityFilter() {
int c = nextCharIgnoreWhitespace();
- if (c == '(') {
+
+ parenFilter: if (c == '(') {
+ // Need to peek ahead to see if this is an outer join.
+ int savedPos = mPos;
+ try {
+ if (Character.isJavaIdentifierStart(nextCharIgnoreWhitespace())) {
+ mPos--;
+ parseIdentifier();
+ if (nextCharIgnoreWhitespace() == ')') {
+ // Is an outer join, so back up.
+ break parenFilter;
+ }
+ }
+ } finally {
+ mPos = savedPos;
+ }
Filter<S> test = parseFilter();
c = nextCharIgnoreWhitespace();
if (c != ')') {
@@ -123,31 +138,31 @@ class FilterParser<S extends Storable> {
throw error("Right paren expected");
}
return test;
- } else {
+ }
+
+ mPos--;
+ ChainedProperty<S> chained = parseChainedProperty();
+ c = nextCharIgnoreWhitespace();
+ if (c != '(') {
mPos--;
- ChainedProperty<S> chained = parseChainedProperty();
- c = nextCharIgnoreWhitespace();
- if (c != '(') {
- mPos--;
- return parsePropertyFilter(chained);
- }
+ return parsePropertyFilter(chained);
+ }
- Filter<?> subFilter;
+ Filter<?> subFilter;
+ c = nextCharIgnoreWhitespace();
+ if (c == ')') {
+ subFilter = null;
+ } else {
+ mPos--;
+ subFilter = parseChainedFilter(chained);
c = nextCharIgnoreWhitespace();
- if (c == ')') {
- subFilter = null;
- } else {
+ if (c != ')') {
mPos--;
- subFilter = parseChainedFilter(chained);
- c = nextCharIgnoreWhitespace();
- if (c != ')') {
- mPos--;
- throw error("Right paren expected");
- }
+ throw error("Right paren expected");
}
-
- return ExistsFilter.build(chained, subFilter, false);
}
+
+ return ExistsFilter.build(chained, subFilter, false);
}
private PropertyFilter<S> parsePropertyFilter(ChainedProperty<S> chained) {
@@ -239,6 +254,18 @@ class FilterParser<S extends Storable> {
@SuppressWarnings("unchecked")
ChainedProperty<S> parseChainedProperty() {
+ List<Boolean> outerJoinList = null;
+ int lastOuterJoinPos = -1;
+
+ if (nextChar() == '(') {
+ lastOuterJoinPos = mPos - 1;
+ // Skip any whitespace after paren.
+ nextCharIgnoreWhitespace();
+ outerJoinList = new ArrayList<Boolean>(4);
+ outerJoinList.add(true);
+ }
+ mPos--;
+
String ident = parseIdentifier();
StorableProperty<S> prime =
StorableIntrospector.examine(mType).getAllProperties().get(ident);
@@ -248,8 +275,21 @@ class FilterParser<S extends Storable> {
mType.getName() + '"');
}
+ if (outerJoinList != null && outerJoinList.get(0)) {
+ if (nextCharIgnoreWhitespace() != ')') {
+ mPos--;
+ throw error("Right paren expected");
+ }
+ }
+
if (nextCharIgnoreWhitespace() != '.') {
mPos--;
+ if (outerJoinList != null && outerJoinList.get(0)) {
+ if (lastOuterJoinPos >= 0) {
+ mPos = lastOuterJoinPos;
+ }
+ throw error("Outer join not allowed for non-chained property");
+ }
return ChainedProperty.get(prime);
}
@@ -257,7 +297,28 @@ class FilterParser<S extends Storable> {
Class<?> type = prime.getType();
while (true) {
+ lastOuterJoinPos = -1;
+
+ if (nextChar() == '(') {
+ lastOuterJoinPos = mPos - 1;
+ // Skip any whitespace after paren.
+ nextCharIgnoreWhitespace();
+ if (outerJoinList == null) {
+ outerJoinList = new ArrayList<Boolean>(4);
+ // Fill in false values.
+ outerJoinList.add(false); // prime is inner join
+ for (int i=chain.size(); --i>=0; ) {
+ outerJoinList.add(false);
+ }
+ }
+ outerJoinList.add(true);
+ } else if (outerJoinList != null) {
+ outerJoinList.add(false);
+ }
+ mPos--;
+
ident = parseIdentifier();
+
if (Storable.class.isAssignableFrom(type)) {
StorableInfo<?> info =
StorableIntrospector.examine((Class<? extends Storable>) type);
@@ -274,14 +335,38 @@ class FilterParser<S extends Storable> {
throw error("Property \"" + ident + "\" not found for type \"" +
type.getName() + "\" because it has no properties");
}
+
+ if (outerJoinList != null && outerJoinList.get(outerJoinList.size() - 1)) {
+ if (nextCharIgnoreWhitespace() != ')') {
+ mPos--;
+ throw error("Right paren expected");
+ }
+ }
+
if (nextCharIgnoreWhitespace() != '.') {
mPos--;
break;
}
}
+ boolean[] outerJoin = null;
+ if (outerJoinList != null) {
+ if (outerJoinList.get(outerJoinList.size() - 1)) {
+ if (lastOuterJoinPos >= 0) {
+ mPos = lastOuterJoinPos;
+ }
+ throw error("Outer join not allowed for last property in chain");
+ }
+ outerJoin = new boolean[outerJoinList.size()];
+ for (int i=outerJoinList.size(); --i>=0; ) {
+ outerJoin[i] = outerJoinList.get(i);
+ }
+ }
+
return ChainedProperty
- .get(prime, (StorableProperty<?>[]) chain.toArray(new StorableProperty[chain.size()]));
+ .get(prime,
+ (StorableProperty<?>[]) chain.toArray(new StorableProperty[chain.size()]),
+ outerJoin);
}
private String parseIdentifier() {
diff --git a/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java b/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java
index c4a585d..c3c5c79 100644
--- a/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java
+++ b/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java
@@ -26,6 +26,7 @@ import org.cojen.classfile.TypeDesc;
import com.amazon.carbonado.Storable;
import com.amazon.carbonado.info.ChainedProperty;
+import com.amazon.carbonado.info.StorableProperty;
/**
* Filter tree node that performs a relational test against a specific property
@@ -100,6 +101,10 @@ public class PropertyFilter<S extends Storable> extends Filter<S> {
if (property == null || op == null) {
throw new IllegalArgumentException();
}
+ if (property.isOuterJoin(property.getChainCount())) {
+ throw new IllegalArgumentException
+ ("Last property in chain cannot be an outer join: " + property);
+ }
mProperty = property;
mOp = op;
mBindID = bindID;
@@ -108,10 +113,35 @@ public class PropertyFilter<S extends Storable> extends Filter<S> {
@Override
public PropertyFilter<S> not() {
+ ChainedProperty<S> property = mProperty;
+
+ if (property.getChainCount() > 0) {
+ // Flip inner/outer joins.
+
+ int chainCount = property.getChainCount();
+ StorableProperty<?>[] chain = new StorableProperty[chainCount];
+ for (int i=0; i<chainCount; i++) {
+ chain[i] = property.getChainedProperty(i);
+ }
+
+ boolean[] outerJoin = null;
+ // Flip all but the last property in the chain.
+ for (int i=0; i<chainCount; i++) {
+ if (!property.isOuterJoin(i)) {
+ if (outerJoin == null) {
+ outerJoin = new boolean[chainCount + 1];
+ }
+ outerJoin[i] = true;
+ }
+ }
+
+ property = ChainedProperty.get(property.getPrimeProperty(), chain, outerJoin);
+ }
+
if (mBindID == BOUND_CONSTANT) {
- return getCanonical(mProperty, mOp.reverse(), mConstant);
+ return getCanonical(property, mOp.reverse(), mConstant);
} else {
- return getCanonical(mProperty, mOp.reverse(), mBindID);
+ return getCanonical(property, mOp.reverse(), mBindID);
}
}
diff --git a/src/main/java/com/amazon/carbonado/info/ChainedProperty.java b/src/main/java/com/amazon/carbonado/info/ChainedProperty.java
index e067350..71f1ba5 100644
--- a/src/main/java/com/amazon/carbonado/info/ChainedProperty.java
+++ b/src/main/java/com/amazon/carbonado/info/ChainedProperty.java
@@ -29,7 +29,7 @@ import com.amazon.carbonado.Storable;
import com.amazon.carbonado.util.Appender;
/**
- * Represents a property to query against or to order by. Properties may be
+ * Represents a property to filter on or to order by. Properties may be
* specified in a simple form, like "firstName", or in a chained form, like
* "address.state". In both forms, the first property is the "prime"
* property. All properties that follow are chained.
@@ -46,7 +46,7 @@ public class ChainedProperty<S extends Storable> implements Appender {
*/
@SuppressWarnings("unchecked")
public static <S extends Storable> ChainedProperty<S> get(StorableProperty<S> prime) {
- return (ChainedProperty<S>) cCanonical.put(new ChainedProperty<S>(prime, null));
+ return (ChainedProperty<S>) cCanonical.put(new ChainedProperty<S>(prime, null, null));
}
/**
@@ -58,7 +58,22 @@ public class ChainedProperty<S extends Storable> implements Appender {
@SuppressWarnings("unchecked")
public static <S extends Storable> ChainedProperty<S> get(StorableProperty<S> prime,
StorableProperty<?>... chain) {
- return (ChainedProperty<S>) cCanonical.put(new ChainedProperty<S>(prime, chain));
+ return (ChainedProperty<S>) cCanonical.put(new ChainedProperty<S>(prime, chain, null));
+ }
+
+ /**
+ * Returns a canonical instance.
+ *
+ * @throws IllegalArgumentException if prime is null or if chained
+ * properties are not formed properly
+ * @since 1.2
+ */
+ @SuppressWarnings("unchecked")
+ public static <S extends Storable> ChainedProperty<S> get(StorableProperty<S> prime,
+ StorableProperty<?>[] chain,
+ boolean[] outerJoin) {
+ return (ChainedProperty<S>) cCanonical.put
+ (new ChainedProperty<S>(prime, chain, outerJoin));
}
/**
@@ -82,12 +97,20 @@ public class ChainedProperty<S extends Storable> implements Appender {
String name;
if (dot < 0) {
- name = str;
+ name = str.trim();
} else {
- name = str.substring(pos, dot);
+ name = str.substring(pos, dot).trim();
pos = dot + 1;
}
+ List<Boolean> outerJoinList = null;
+
+ if (name.startsWith("(") && name.endsWith(")")) {
+ outerJoinList = new ArrayList<Boolean>(4);
+ outerJoinList.add(true);
+ name = name.substring(1, name.length() - 1).trim();
+ }
+
StorableProperty<S> prime = info.getAllProperties().get(name);
if (prime == null) {
@@ -97,7 +120,11 @@ public class ChainedProperty<S extends Storable> implements Appender {
}
if (pos <= 0) {
- return get(prime);
+ if (outerJoinList == null || !outerJoinList.get(0)) {
+ return get(prime);
+ } else {
+ return get(prime, null, new boolean[] {true});
+ }
}
List<StorableProperty<?>> chain = new ArrayList<StorableProperty<?>>(4);
@@ -106,12 +133,28 @@ public class ChainedProperty<S extends Storable> implements Appender {
while (pos > 0) {
dot = str.indexOf('.', pos);
if (dot < 0) {
- name = str.substring(pos);
+ name = str.substring(pos).trim();
pos = -1;
} else {
- name = str.substring(pos, dot);
+ name = str.substring(pos, dot).trim();
pos = dot + 1;
}
+
+ if (name.startsWith("(") && name.endsWith(")")) {
+ if (outerJoinList == null) {
+ outerJoinList = new ArrayList<Boolean>(4);
+ // Fill in false values.
+ outerJoinList.add(false); // prime is inner join
+ for (int i=chain.size(); --i>=0; ) {
+ outerJoinList.add(false);
+ }
+ }
+ outerJoinList.add(true);
+ name = name.substring(1, name.length() - 1).trim();
+ } else if (outerJoinList != null) {
+ outerJoinList.add(false);
+ }
+
if (Storable.class.isAssignableFrom(type)) {
StorableInfo propInfo =
StorableIntrospector.examine((Class<? extends Storable>) type);
@@ -131,24 +174,51 @@ public class ChainedProperty<S extends Storable> implements Appender {
}
}
+ boolean[] outerJoin = null;
+ if (outerJoinList != null) {
+ outerJoin = new boolean[outerJoinList.size()];
+ for (int i=outerJoinList.size(); --i>=0; ) {
+ outerJoin[i] = outerJoinList.get(i);
+ }
+ }
+
return get(prime,
- (StorableProperty<?>[]) chain.toArray(new StorableProperty[chain.size()]));
+ (StorableProperty<?>[]) chain.toArray(new StorableProperty[chain.size()]),
+ outerJoin);
}
private final StorableProperty<S> mPrime;
private final StorableProperty<?>[] mChain;
+ private final boolean[] mOuterJoin;
/**
* @param prime must not be null
* @param chain can be null if none
- * @throws IllegalArgumentException if prime is null
+ * @param outerJoin can be null for all inner joins
+ * @throws IllegalArgumentException if prime is null or if outer join chain is too long
*/
- private ChainedProperty(StorableProperty<S> prime, StorableProperty<?>[] chain) {
+ private ChainedProperty(StorableProperty<S> prime, StorableProperty<?>[] chain,
+ boolean[] outerJoin)
+ {
if (prime == null) {
- throw new IllegalArgumentException();
+ throw new IllegalArgumentException("No prime property");
}
+
mPrime = prime;
mChain = (chain == null || chain.length == 0) ? null : chain.clone();
+
+ if (outerJoin != null) {
+ int expectedLength = (chain == null ? 0 : chain.length) + 1;
+ if (outerJoin.length > expectedLength) {
+ throw new IllegalArgumentException
+ ("Outer join array too long: " + outerJoin.length + " > " + expectedLength);
+ }
+ boolean[] newOuterJoin = new boolean[expectedLength];
+ System.arraycopy(outerJoin, 0, newOuterJoin, 0, outerJoin.length);
+ outerJoin = newOuterJoin;
+ }
+
+ mOuterJoin = outerJoin;
}
public StorableProperty<S> getPrimeProperty() {
@@ -219,6 +289,9 @@ public class ChainedProperty<S extends Storable> implements Appender {
return mChain == null ? 0 : mChain.length;
}
+ /**
+ * @param index valid range is 0 to chainCount - 1
+ */
public StorableProperty<?> getChainedProperty(int index) throws IndexOutOfBoundsException {
if (mChain == null) {
throw new IndexOutOfBoundsException();
@@ -228,24 +301,73 @@ public class ChainedProperty<S extends Storable> implements Appender {
}
/**
+ * Returns true if the property at the given index should be treated as an
+ * outer join. Index zero is the prime property.
+ *
+ * @param index valid range is 0 to chainCount
+ * @since 1.2
+ */
+ public boolean isOuterJoin(int index) throws IndexOutOfBoundsException {
+ if (index < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (mOuterJoin == null) {
+ if (index > getChainCount()) {
+ throw new IndexOutOfBoundsException();
+ }
+ return false;
+ }
+ return mOuterJoin[index];
+ }
+
+ /**
* Returns a new ChainedProperty with another property appended.
*/
public ChainedProperty<S> append(StorableProperty<?> property) {
+ return append(property, false);
+ }
+
+ /**
+ * Returns a new ChainedProperty with another property appended.
+ *
+ * @param outerJoin pass true for outer join
+ * @since 1.2
+ */
+ public ChainedProperty<S> append(StorableProperty<?> property, boolean outerJoin) {
+ if (property == null) {
+ throw new IllegalArgumentException();
+ }
+
StorableProperty<?>[] newChain = new StorableProperty[getChainCount() + 1];
if (newChain.length > 1) {
System.arraycopy(mChain, 0, newChain, 0, mChain.length);
}
newChain[newChain.length - 1] = property;
- return get(mPrime, newChain);
+
+ boolean[] newOuterJoin = mOuterJoin;
+
+ if (outerJoin) {
+ newOuterJoin = new boolean[newChain.length + 1];
+ if (mOuterJoin != null) {
+ System.arraycopy(mOuterJoin, 0, newOuterJoin, 0, mOuterJoin.length);
+ }
+ newOuterJoin[newOuterJoin.length - 1] = true;
+ }
+
+ return get(mPrime, newChain, newOuterJoin);
}
/**
* Returns a new ChainedProperty with another property appended.
*/
public ChainedProperty<S> append(ChainedProperty<?> property) {
+ if (property == null) {
+ throw new IllegalArgumentException();
+ }
+
final int propChainCount = property.getChainCount();
if (propChainCount == 0) {
- return append(property.getPrimeProperty());
+ return append(property.getPrimeProperty(), property.isOuterJoin(0));
}
StorableProperty<?>[] newChain =
@@ -262,7 +384,19 @@ public class ChainedProperty<S extends Storable> implements Appender {
newChain[pos++] = property.getChainedProperty(i);
}
- return get(mPrime, newChain);
+ boolean[] newOuterJoin = mOuterJoin;
+
+ if (property.mOuterJoin != null) {
+ newOuterJoin = new boolean[newChain.length + 1];
+ if (mOuterJoin != null) {
+ System.arraycopy(mOuterJoin, 0, newOuterJoin, 0, mOuterJoin.length);
+ }
+ System.arraycopy(property.mOuterJoin, 0,
+ newOuterJoin, mOuterJoin.length,
+ property.mOuterJoin.length);
+ }
+
+ return get(mPrime, newChain, newOuterJoin);
}
/**
@@ -275,11 +409,23 @@ public class ChainedProperty<S extends Storable> implements Appender {
throw new IllegalStateException();
}
if (getChainCount() == 1) {
- return get(mPrime);
+ if (!isOuterJoin(0)) {
+ return get(mPrime);
+ } else {
+ return get(mPrime, null, new boolean[] {true});
+ }
}
StorableProperty<?>[] newChain = new StorableProperty[getChainCount() - 1];
System.arraycopy(mChain, 0, newChain, 0, newChain.length);
- return get(mPrime, newChain);
+
+ boolean[] newOuterJoin = mOuterJoin;
+
+ if (newOuterJoin != null && newOuterJoin.length > (newChain.length + 1)) {
+ newOuterJoin = new boolean[newChain.length + 1];
+ System.arraycopy(mOuterJoin, 0, newOuterJoin, 0, newChain.length + 1);
+ }
+
+ return get(mPrime, newChain, newOuterJoin);
}
/**
@@ -293,11 +439,23 @@ public class ChainedProperty<S extends Storable> implements Appender {
throw new IllegalStateException();
}
if (getChainCount() == 1) {
- return get(mChain[0]);
+ if (!isOuterJoin(1)) {
+ return get(mChain[0]);
+ } else {
+ return get(mChain[0], null, new boolean[] {true});
+ }
}
StorableProperty<?>[] newChain = new StorableProperty[getChainCount() - 1];
System.arraycopy(mChain, 1, newChain, 0, newChain.length);
- return get(mChain[0], newChain);
+
+ boolean[] newOuterJoin = mOuterJoin;
+
+ if (newOuterJoin != null) {
+ newOuterJoin = new boolean[newChain.length + 1];
+ System.arraycopy(mOuterJoin, 1, newOuterJoin, 0, mOuterJoin.length - 1);
+ }
+
+ return get(mChain[0], newChain, newOuterJoin);
}
@Override
@@ -309,6 +467,14 @@ public class ChainedProperty<S extends Storable> implements Appender {
hash = hash * 31 + chain[i].hashCode();
}
}
+ boolean[] outerJoin = mOuterJoin;
+ if (outerJoin != null) {
+ for (int i=outerJoin.length; --i>=0; ) {
+ if (outerJoin[i]) {
+ hash += 1;
+ }
+ }
+ }
return hash;
}
@@ -324,8 +490,18 @@ public class ChainedProperty<S extends Storable> implements Appender {
// equals method. Otherwise, canonical ChainedProperty instances
// may refer to StorableProperty instances which are no longer
// available through the Introspector.
- return getType() == other.getType() && mPrime == other.mPrime
- && identityEquals(mChain, other.mChain);
+ if (getType() == other.getType() && mPrime == other.mPrime
+ && identityEquals(mChain, other.mChain))
+ {
+ // Compare outer joins.
+ int count = getChainCount() + 1;
+ for (int i=0; i<count; i++) {
+ if (isOuterJoin(i) != other.isOuterJoin(i)) {
+ return false;
+ }
+ }
+ return true;
+ }
}
return false;
}
@@ -362,7 +538,7 @@ public class ChainedProperty<S extends Storable> implements Appender {
*/
@Override
public String toString() {
- if (mChain == null) {
+ if (mChain == null && !isOuterJoin(0)) {
return mPrime.getName();
}
StringBuilder buf = new StringBuilder();
@@ -380,7 +556,7 @@ public class ChainedProperty<S extends Storable> implements Appender {
* many-to-one joins.
*/
public void appendTo(Appendable app) throws IOException {
- app.append(mPrime.getName());
+ appendPropTo(app, mPrime.getName(), isOuterJoin(0));
StorableProperty<?>[] chain = mChain;
if (chain != null) {
app.append('.');
@@ -388,8 +564,18 @@ public class ChainedProperty<S extends Storable> implements Appender {
if (i > 0) {
app.append('.');
}
- app.append(chain[i].getName());
+ appendPropTo(app, chain[i].getName(), isOuterJoin(i + 1));
}
}
}
+
+ private void appendPropTo(Appendable app, String name, boolean outer) throws IOException {
+ if (outer) {
+ app.append('(');
+ }
+ app.append(name);
+ if (outer) {
+ app.append(')');
+ }
+ }
}
diff --git a/src/main/java/com/amazon/carbonado/qe/IndexedQueryAnalyzer.java b/src/main/java/com/amazon/carbonado/qe/IndexedQueryAnalyzer.java
index 60cf3da..93d4666 100644
--- a/src/main/java/com/amazon/carbonado/qe/IndexedQueryAnalyzer.java
+++ b/src/main/java/com/amazon/carbonado/qe/IndexedQueryAnalyzer.java
@@ -209,12 +209,20 @@ public class IndexedQueryAnalyzer<S extends Storable> {
if (!isProperJoin(chainedProp.getPrimeProperty())) {
break evaluate;
}
+ if (chainedProp.isOuterJoin(0)) {
+ // Outer joins cannot be optimized via foreign indexes.
+ break evaluate;
+ }
int count = chainedProp.getChainCount();
for (int i=0; i<count; i++) {
if (!isProperJoin(chainedProp.getChainedProperty(i))) {
break evaluate;
}
+ if (chainedProp.isOuterJoin(i + 1)) {
+ // Outer joins cannot be optimized via foreign indexes.
+ break evaluate;
+ }
}
// All foreign indexes are available for use.
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 855cce4..2a1212a 100644
--- a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorage.java
+++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorage.java
@@ -846,11 +846,45 @@ class JDBCStorage<S extends Storable> extends StandardQueryFactory<S>
private static class JoinNode {
// Joined property which led to this node. For root node, it is null.
private final JDBCStorableProperty<?> mProperty;
+ private final boolean mOuterJoin;
private final JDBCStorableInfo<?> mInfo;
private final String mAlias;
- private final Map<String, JoinNode> mSubNodes;
+ private static class SubNodeKey {
+ final String mPropertyName;
+ final boolean mOuterJoin;
+
+ SubNodeKey(String propertyName, boolean outerJoin) {
+ mPropertyName = propertyName;
+ mOuterJoin = outerJoin;
+ }
+
+ @Override
+ public int hashCode() {
+ return mPropertyName.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof SubNodeKey) {
+ SubNodeKey other = (SubNodeKey) obj;
+ return mPropertyName.equals(other.mPropertyName)
+ && mOuterJoin == other.mOuterJoin;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "propertyName=" + mPropertyName + ", outerJoin=" + mOuterJoin;
+ }
+ }
+
+ private final Map<SubNodeKey, JoinNode> mSubNodes;
private boolean mAliasRequired;
@@ -858,15 +892,19 @@ class JDBCStorage<S extends Storable> extends StandardQueryFactory<S>
* @param alias table alias in SQL statement, i.e. "T1"
*/
JoinNode(JDBCStorableInfo<?> info, String alias) {
- this(null, info, alias);
+ this(null, false, info, alias);
}
- private JoinNode(JDBCStorableProperty<?> property, JDBCStorableInfo<?> info, String alias)
+ private JoinNode(JDBCStorableProperty<?> property,
+ boolean outerJoin,
+ JDBCStorableInfo<?> info,
+ String alias)
{
mProperty = property;
+ mOuterJoin = outerJoin;
mInfo = info;
mAlias = alias;
- mSubNodes = new LinkedHashMap<String, JoinNode>();
+ mSubNodes = new LinkedHashMap<SubNodeKey, JoinNode>();
}
/**
@@ -891,8 +929,9 @@ class JDBCStorage<S extends Storable> extends StandardQueryFactory<S>
} else {
property = chained.getChainedProperty(offset - 1);
}
- String name = property.getName();
- JoinNode subNode = mSubNodes.get(name);
+ boolean outerJoin = chained.isOuterJoin(offset);
+ SubNodeKey key = new SubNodeKey(property.getName(), outerJoin);
+ JoinNode subNode = mSubNodes.get(key);
if (subNode != null) {
return subNode.findAliasFor(chained, offset + 1);
}
@@ -931,15 +970,11 @@ class JDBCStorage<S extends Storable> extends StandardQueryFactory<S>
private void appendTailJoinTo(StatementBuilder fromClause) {
for (JoinNode jn : mSubNodes.values()) {
- // TODO: By default, joins are all inner. A join could become
- // LEFT OUTER JOIN if the query filter has a term like this:
- // "address = ? | address.state = ?", and the runtime value of
- // address is null. Because of DNF transformation and lack of
- // short-circuit ops, this syntax might be difficult to parse.
- // This might be a better way of expressing an outer join:
- // "address(.)state = ?".
-
- fromClause.append(" INNER JOIN");
+ if (jn.mOuterJoin) {
+ fromClause.append(" LEFT OUTER JOIN");
+ } else {
+ fromClause.append(" INNER JOIN");
+ }
jn.appendTableNameAndAliasTo(fromClause);
fromClause.append(" ON ");
int count = jn.mProperty.getJoinElementCount();
@@ -984,13 +1019,14 @@ class JDBCStorage<S extends Storable> extends StandardQueryFactory<S>
} else {
property = chained.getChainedProperty(offset - 1);
}
- String name = property.getName();
- JoinNode subNode = mSubNodes.get(name);
+ boolean outerJoin = chained.isOuterJoin(offset);
+ SubNodeKey key = new SubNodeKey(property.getName(), outerJoin);
+ JoinNode subNode = mSubNodes.get(key);
if (subNode == null) {
JDBCStorableInfo<?> info = repository.examineStorable(property.getJoinedType());
JDBCStorableProperty<?> jProperty = repository.getJDBCStorableProperty(property);
- subNode = new JoinNode(jProperty, info, aliasGenerator.nextAlias());
- mSubNodes.put(name, subNode);
+ subNode = new JoinNode(jProperty, outerJoin, info, aliasGenerator.nextAlias());
+ mSubNodes.put(key, subNode);
}
subNode.addJoin(repository, chained, aliasGenerator, offset + 1);
}