From 97af4be638e371a2f693bde2798fc233a143f3f9 Mon Sep 17 00:00:00 2001 From: "Brian S. O'Neill" Date: Sun, 29 Apr 2007 17:47:50 +0000 Subject: Merged in support for derived properties. --- src/main/java/com/amazon/carbonado/Derived.java | 62 +++ .../amazon/carbonado/OptimisticLockException.java | 32 ++ src/main/java/com/amazon/carbonado/Version.java | 42 +- .../com/amazon/carbonado/gen/CodeBuilderUtil.java | 50 +- .../carbonado/gen/MasterStorableGenerator.java | 178 +++++-- .../amazon/carbonado/gen/StorableGenerator.java | 181 ++++--- .../amazon/carbonado/gen/StorableSerializer.java | 4 +- .../carbonado/info/StorableIntrospector.java | 527 +++++++++++++++++++-- .../amazon/carbonado/info/StorableProperty.java | 36 +- .../java/com/amazon/carbonado/layout/Layout.java | 2 +- .../com/amazon/carbonado/qe/AbstractQuery.java | 6 + .../java/com/amazon/carbonado/qe/EmptyQuery.java | 18 + .../carbonado/raw/GenericEncodingStrategy.java | 11 +- .../amazon/carbonado/raw/GenericStorableCodec.java | 2 +- .../repo/indexed/DependentStorableFetcher.java | 174 +++++++ .../repo/indexed/DerivedIndexesTrigger.java | 164 +++++++ .../carbonado/repo/indexed/IndexAnalysis.java | 132 ++++++ .../carbonado/repo/indexed/IndexEntryAccessor.java | 2 +- .../carbonado/repo/indexed/IndexedRepository.java | 9 +- .../carbonado/repo/indexed/IndexedStorage.java | 25 +- .../carbonado/repo/indexed/ManagedIndex.java | 23 +- .../carbonado/repo/jdbc/JDBCStorableGenerator.java | 18 +- .../repo/jdbc/JDBCStorableIntrospector.java | 21 +- .../com/amazon/carbonado/spi/WrappedQuery.java | 17 + .../SyntheticStorableReferenceBuilder.java | 17 +- 25 files changed, 1524 insertions(+), 229 deletions(-) create mode 100644 src/main/java/com/amazon/carbonado/Derived.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/DependentStorableFetcher.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/DerivedIndexesTrigger.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/IndexAnalysis.java diff --git a/src/main/java/com/amazon/carbonado/Derived.java b/src/main/java/com/amazon/carbonado/Derived.java new file mode 100644 index 0000000..77ffd2a --- /dev/null +++ b/src/main/java/com/amazon/carbonado/Derived.java @@ -0,0 +1,62 @@ +/* + * 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; + +import java.lang.annotation.*; + +/** + * Identifies a {@link Storable} property which is not directly persisted, but + * is instead derived from other property values. A derived property cannot be + * abstract, and a "set" method is optional. + * + *

Derived properties can be used just like a normal property in most + * cases. They can be used in query filters, indexes, alternate keys, and they + * can also be used to define a {@link Version} property. + * + *

If the derived property depends on {@link Join} properties and is also + * used in an index or alternate key, dependencies must be listed in order for + * the index to be properly updated. + * + *

Example:

+ * @Indexes(@Index("uppercaseName"))
+ * public abstract class UserInfo implements Storable<UserInfo> {
+ *     /**
+ *      * Derive an uppercase name for case-insensitive searches.
+ *      */
+ *     @Derived
+ *     public String getUppercaseName() {
+ *         String name = getName();
+ *         return name == null ? null : name.toUpperCase();
+ *     }
+ *
+ *     ...
+ * }
+ * 
+ * + * @author Brian S O'Neill + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Derived { + /** + * List of properties that this property is derived from. + */ + String[] from() default {}; +} diff --git a/src/main/java/com/amazon/carbonado/OptimisticLockException.java b/src/main/java/com/amazon/carbonado/OptimisticLockException.java index ccd894c..03004c2 100644 --- a/src/main/java/com/amazon/carbonado/OptimisticLockException.java +++ b/src/main/java/com/amazon/carbonado/OptimisticLockException.java @@ -88,6 +88,18 @@ public class OptimisticLockException extends PersistException { mStorable = s; } + /** + * Construct exception for when new version was expected to have increased. + * + * @param savedVersion actual persistent version number of storable + * @param s Storable which was acted upon + * @param newVersion new version which was provided + */ + public OptimisticLockException(Object savedVersion, Storable s, Object newVersion) { + super(makeMessage(savedVersion, s, newVersion)); + mStorable = s; + } + /** * Returns the Storable which was acted upon, or null if not available. */ @@ -114,4 +126,24 @@ public class OptimisticLockException extends PersistException { return message; } + + private static String makeMessage(Object savedVersion, Storable s, Object newVersion) { + String message; + if (savedVersion == null && newVersion == null) { + message = "New version is not larger than existing version"; + } else { + message = "New version of " + newVersion + + " is not larger than existing version of " + savedVersion; + } + + if (s != null) { + if (message == null) { + message = s.toStringKeyOnly(); + } else { + message = message + ": " + s.toStringKeyOnly(); + } + } + + return message; + } } diff --git a/src/main/java/com/amazon/carbonado/Version.java b/src/main/java/com/amazon/carbonado/Version.java index 7c3c2b4..cb5dcb8 100644 --- a/src/main/java/com/amazon/carbonado/Version.java +++ b/src/main/java/com/amazon/carbonado/Version.java @@ -25,34 +25,22 @@ import java.lang.annotation.*; * number for the entire Storable instance. Only one property can have this * designation. * - *

Support for the version property falls into three categories. A - * repository may manage the version; it may respect the version; or it may - * merely check the version. + *

Philosophically, a version property can be considered part of the + * identity of the storable. Unless the version is {@link Derived}, the + * repository is responsibile for establishing the version on insert, and for + * auto-incrementing it on update. Under no circumstances should a normal + * version property be incremented manually; this can result in a false {@link + * OptimisticLockException}, or worse may allow the persistent record to become + * corrupted. * - *

Manage: Each storable with a version property must have one and - * only one repository which is responsible for managing the version property. - * That repository takes responsibility for establishing the version on insert, - * and for auto-incrementing it on update. Under no circumstances should the - * version property be incremented manually; this can result in a false - * optimistic lock exception, or worse may allow the persistent record to - * become corrupted. Prior to incrementing, these repositories will verify - * that the version exactly matches the version of the current record, throwing - * an {@link OptimisticLockException} otherwise. The JDBC repository is the - * canonical example of this sort of repository. - * - *

Respect: Repositories which respect the version use the version to - * guarantee that updates are idempotent -- that is, that an update is applied - * once and only once. These repositories will check that the version property - * is strictly greater than the version of the current record, and will - * (silently) ignore changes which fail this check. - * - *

Check: Philosophically, a version property can be considered part - * of the identity of the storable. That is, if the storable has a version - * property, it cannot be considered fully specified unless that property is - * specified. Thus, the minimal required support for all repositories is to - * check that the version is specified on update. All repositories -- even - * those which neither check nor manage the version -- will throw an {@link - * IllegalStateException} if the version property is not set before update. + *

When updating a storable which has a normal version property, a value for + * the version must be specified along with its primary key. Otherwise, an + * {@link IllegalStateException} is thrown when calling update. If the update + * operation detects that the specified version doesn't exactly match the + * version of the existing persisted storable, an {@link + * OptimisticLockException} is thrown. For {@link Derived} versions, an {@link + * OptimisticLockException} is thrown only if the update detects that the new + * version hasn't incremented. * *

The actual type of the version property can be anything, but some * repositories might only support integers. For maximum portability, version diff --git a/src/main/java/com/amazon/carbonado/gen/CodeBuilderUtil.java b/src/main/java/com/amazon/carbonado/gen/CodeBuilderUtil.java index c1f26a0..1b754fc 100644 --- a/src/main/java/com/amazon/carbonado/gen/CodeBuilderUtil.java +++ b/src/main/java/com/amazon/carbonado/gen/CodeBuilderUtil.java @@ -267,7 +267,9 @@ public class CodeBuilderUtil { /** * Generates code to compare two values on the stack, and branch to the - * provided Label if they are not equal. Both values must be of the same type. + * provided Label if they are not equal. Both values must be of the same + * type. If they are floating point values, NaN is considered equal to NaN, + * which is inconsistent with the usual treatment for NaN. * *

The generated instruction consumes both values on the stack. * @@ -286,18 +288,25 @@ public class CodeBuilderUtil { final boolean choice) { if (valueType.getTypeCode() != TypeDesc.OBJECT_CODE) { - b.ifComparisonBranch(label, choice ? "==" : "!=", valueType); + if (valueType.getTypeCode() == TypeDesc.FLOAT_CODE) { + // Special treatment to handle NaN. + b.invokeStatic(TypeDesc.FLOAT.toObjectType(), "compare", TypeDesc.INT, + new TypeDesc[] {TypeDesc.FLOAT, TypeDesc.FLOAT}); + b.ifZeroComparisonBranch(label, choice ? "==" : "!="); + } else if (valueType.getTypeCode() == TypeDesc.DOUBLE_CODE) { + // Special treatment to handle NaN. + b.invokeStatic(TypeDesc.DOUBLE.toObjectType(), "compare", TypeDesc.INT, + new TypeDesc[] {TypeDesc.DOUBLE, TypeDesc.DOUBLE}); + b.ifZeroComparisonBranch(label, choice ? "==" : "!="); + } else { + b.ifComparisonBranch(label, choice ? "==" : "!=", valueType); + } return; } - // Equals method returns zero for false, so if choice is true, branch - // if not zero. Note that operator selection is opposite when invoking - // a direct ifComparisonBranch method. - String equalsBranchOp = choice ? "!=" : "=="; - if (!testForNull) { - addEqualsCallTo(b, valueType); - b.ifZeroComparisonBranch(label, equalsBranchOp); + String op = addEqualsCallTo(b, valueType, choice); + b.ifZeroComparisonBranch(label, op); return; } @@ -318,14 +327,19 @@ public class CodeBuilderUtil { isNotNull.setLocation(); b.loadLocal(value); b.swap(); - addEqualsCallTo(b, valueType); - b.ifZeroComparisonBranch(label, equalsBranchOp); + String op = addEqualsCallTo(b, valueType, choice); + b.ifZeroComparisonBranch(label, op); cont.setLocation(); } - public static void addEqualsCallTo(CodeBuilder b, TypeDesc fieldType) { + /** + * @param fieldType must be an object type + * @return zero comparison branch operator + */ + private static String addEqualsCallTo(CodeBuilder b, TypeDesc fieldType, boolean choice) { if (fieldType.isArray()) { + // FIXME: Array comparisons don't handle desired comparison of NaN. if (!fieldType.getComponentType().isPrimitive()) { TypeDesc type = TypeDesc.forClass(Object[].class); b.invokeStatic("java.util.Arrays", "deepEquals", @@ -334,6 +348,17 @@ public class CodeBuilderUtil { b.invokeStatic("java.util.Arrays", "equals", TypeDesc.BOOLEAN, new TypeDesc[] {fieldType, fieldType}); } + return choice ? "!=" : "=="; + } else if (fieldType.toPrimitiveType() == TypeDesc.FLOAT) { + // Special treatment to handle NaN. + b.invokeVirtual(TypeDesc.FLOAT.toObjectType(), "compareTo", TypeDesc.INT, + new TypeDesc[] {TypeDesc.FLOAT.toObjectType()}); + return choice ? "==" : "!="; + } else if (fieldType.toPrimitiveType() == TypeDesc.DOUBLE) { + // Special treatment to handle NaN. + b.invokeVirtual(TypeDesc.DOUBLE.toObjectType(), "compareTo", TypeDesc.INT, + new TypeDesc[] {TypeDesc.DOUBLE.toObjectType()}); + return choice ? "==" : "!="; } else { TypeDesc[] params = {TypeDesc.OBJECT}; if (fieldType.toClass() != null) { @@ -345,6 +370,7 @@ public class CodeBuilderUtil { } else { b.invokeVirtual(TypeDesc.OBJECT, "equals", TypeDesc.BOOLEAN, params); } + return choice ? "!=" : "=="; } } diff --git a/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java b/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java index 17efb70..581a7d6 100644 --- a/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java +++ b/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java @@ -126,7 +126,7 @@ public final class MasterStorableGenerator { anySequences: if (features.contains(MasterFeature.INSERT_SEQUENCES)) { for (StorableProperty property : info.getAllProperties().values()) { - if (property.getSequenceName() != null) { + if (!property.isDerived() && property.getSequenceName() != null) { break anySequences; } } @@ -266,7 +266,7 @@ public final class MasterStorableGenerator { int ordinal = 0; for (StorableProperty property : mAllProperties.values()) { - if (property.getSequenceName() != null) { + if (!property.isDerived() && property.getSequenceName() != null) { // Check the state of this property, to see if it is // uninitialized. Uninitialized state has value zero. @@ -362,14 +362,16 @@ public final class MasterStorableGenerator { Label tryStart = addEnterTransaction(b, INSERT_OP, txnVar); if (mFeatures.contains(MasterFeature.VERSIONING)) { - // Only set if uninitialized. - b.loadThis(); - b.invokeVirtual(StorableGenerator.IS_VERSION_INITIALIZED_METHOD_NAME, - TypeDesc.BOOLEAN, null); - Label isInitialized = b.createLabel(); - b.ifZeroComparisonBranch(isInitialized, "!="); - addAdjustVersionProperty(b, null, 1); - isInitialized.setLocation(); + if (!mInfo.getVersionProperty().isDerived()) { + // Only set if uninitialized. + b.loadThis(); + b.invokeVirtual(StorableGenerator.IS_VERSION_INITIALIZED_METHOD_NAME, + TypeDesc.BOOLEAN, null); + Label isInitialized = b.createLabel(); + b.ifZeroComparisonBranch(isInitialized, "!="); + addAdjustVersionProperty(b, null, 1); + isInitialized.setLocation(); + } } if (mFeatures.contains(MasterFeature.INSERT_CHECK_REQUIRED)) { @@ -410,7 +412,8 @@ public final class MasterStorableGenerator { for (StorableProperty property : mAllProperties.values()) { ordinal++; - if (property.isJoin() || property.isPrimaryKeyMember() + if (property.isDerived() + || property.isJoin() || property.isPrimaryKeyMember() || property.isNullable() || property.isAutomatic() || property.isVersion()) { @@ -548,33 +551,109 @@ public final class MasterStorableGenerator { b.ifZeroComparisonBranch(failed, "=="); // if (version support enabled) { - // if (this.getVersionNumber() != saved.getVersionNumber()) { - // throw new OptimisticLockException - // (this.getVersionNumber(), saved.getVersionNumber(), this); + // if (!derived version) { + // if (this.getVersionNumber() != saved.getVersionNumber()) { + // throw new OptimisticLockException + // (this.getVersionNumber(), saved.getVersionNumber(), this); + // } + // } else { + // if (this.getVersionNumber() <= saved.getVersionNumber()) { + // throw new OptimisticLockException + // (saved.getVersionNumber(), this, this.getVersionNumber()); + // } // } // } if (mFeatures.contains(MasterFeature.VERSIONING)) { - TypeDesc versionType = TypeDesc.forClass(mInfo.getVersionProperty().getType()); - b.loadThis(); - b.invoke(mInfo.getVersionProperty().getReadMethod()); - b.loadLocal(savedVar); - b.invoke(mInfo.getVersionProperty().getReadMethod()); - Label sameVersion = b.createLabel(); - CodeBuilderUtil.addValuesEqualCall(b, versionType, true, sameVersion, true); - b.newObject(optimisticLockType); - b.dup(); - b.loadThis(); - b.invoke(mInfo.getVersionProperty().getReadMethod()); - b.convert(versionType, TypeDesc.OBJECT); - b.loadLocal(savedVar); - b.invoke(mInfo.getVersionProperty().getReadMethod()); - b.convert(versionType, TypeDesc.OBJECT); - b.loadThis(); - b.invokeConstructor - (optimisticLockType, - new TypeDesc[] {TypeDesc.OBJECT, TypeDesc.OBJECT, storableType}); - b.throwObject(); - sameVersion.setLocation(); + StorableProperty versionProperty = mInfo.getVersionProperty(); + TypeDesc versionType = TypeDesc.forClass(versionProperty.getType()); + + Label allowedVersion = b.createLabel(); + + if (!versionProperty.isDerived()) { + b.loadThis(); + b.invoke(versionProperty.getReadMethod()); + b.loadLocal(savedVar); + b.invoke(versionProperty.getReadMethod()); + CodeBuilderUtil.addValuesEqualCall + (b, versionType, true, allowedVersion, true); + + b.newObject(optimisticLockType); + b.dup(); + b.loadThis(); + b.invoke(versionProperty.getReadMethod()); + b.convert(versionType, TypeDesc.OBJECT); + b.loadLocal(savedVar); + b.invoke(versionProperty.getReadMethod()); + b.convert(versionType, TypeDesc.OBJECT); + b.loadThis(); + b.invokeConstructor + (optimisticLockType, + new TypeDesc[] {TypeDesc.OBJECT, TypeDesc.OBJECT, storableType}); + b.throwObject(); + } else { + b.loadThis(); + b.invoke(versionProperty.getReadMethod()); + LocalVariable newVersion = b.createLocalVariable(null, versionType); + b.storeLocal(newVersion); + + b.loadLocal(savedVar); + b.invoke(versionProperty.getReadMethod()); + LocalVariable savedVersion = b.createLocalVariable(null, versionType); + b.storeLocal(savedVersion); + + // Skip check if new or saved version is null. + branchIfNull(b, newVersion, allowedVersion); + branchIfNull(b, savedVersion, allowedVersion); + + TypeDesc primVersionType = versionType.toPrimitiveType(); + if (primVersionType != null) { + if (versionType != primVersionType) { + b.loadLocal(newVersion); + b.convert(versionType, primVersionType); + newVersion = b.createLocalVariable(null, primVersionType); + b.storeLocal(newVersion); + + b.loadLocal(savedVersion); + b.convert(versionType, primVersionType); + savedVersion = b.createLocalVariable(null, primVersionType); + b.storeLocal(savedVersion); + } + + // Skip check if new or saved version is NaN. + branchIfNaN(b, newVersion, allowedVersion); + branchIfNaN(b, savedVersion, allowedVersion); + + b.loadLocal(newVersion); + b.loadLocal(savedVersion); + b.ifComparisonBranch(allowedVersion, ">", primVersionType); + } else if (Comparable.class.isAssignableFrom(versionProperty.getType())) { + b.loadLocal(newVersion); + b.loadLocal(savedVersion); + b.invokeInterface(TypeDesc.forClass(Comparable.class), "compareTo", + TypeDesc.INT, new TypeDesc[] {TypeDesc.OBJECT}); + b.ifZeroComparisonBranch(allowedVersion, ">"); + } else { + throw new SupportException + ("Derived version property must be Comparable: " + + versionProperty); + } + + b.newObject(optimisticLockType); + b.dup(); + b.loadLocal(savedVar); + b.invoke(versionProperty.getReadMethod()); + b.convert(versionType, TypeDesc.OBJECT); + b.loadThis(); + b.loadThis(); + b.invoke(versionProperty.getReadMethod()); + b.convert(versionType, TypeDesc.OBJECT); + b.invokeConstructor + (optimisticLockType, + new TypeDesc[] {TypeDesc.OBJECT, storableType, TypeDesc.OBJECT}); + b.throwObject(); + } + + allowedVersion.setLocation(); } // this.copyDirtyProperties(saved); @@ -585,7 +664,9 @@ public final class MasterStorableGenerator { b.loadLocal(savedVar); b.invokeVirtual(COPY_DIRTY_PROPERTIES, null, new TypeDesc[] {storableType}); if (mFeatures.contains(MasterFeature.VERSIONING)) { - addAdjustVersionProperty(b, savedVar, -1); + if (!mInfo.getVersionProperty().isDerived()) { + addAdjustVersionProperty(b, savedVar, -1); + } } // if (!saved.doTryUpdateMaster()) { @@ -661,6 +742,31 @@ public final class MasterStorableGenerator { } } + private void branchIfNull(CodeBuilder b, LocalVariable value, Label isNull) { + if (!value.getType().isPrimitive()) { + b.loadLocal(value); + b.ifNullBranch(isNull, true); + } + } + + private void branchIfNaN(CodeBuilder b, LocalVariable value, Label isNaN) { + TypeDesc type = value.getType(); + if (type == TypeDesc.FLOAT || type == TypeDesc.DOUBLE) { + b.loadLocal(value); + if (type == TypeDesc.FLOAT) { + b.invokeStatic(TypeDesc.FLOAT.toObjectType(), + "isNaN", TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.FLOAT}); + b.ifZeroComparisonBranch(isNaN, "!="); + } else { + b.invokeStatic(TypeDesc.DOUBLE.toObjectType(), + "isNaN", TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.DOUBLE}); + b.ifZeroComparisonBranch(isNaN, "!="); + } + } + } + /** * Generates code to enter a transaction, if required. * diff --git a/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java b/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java index 4934db3..a28f7e6 100644 --- a/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java +++ b/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java @@ -193,7 +193,7 @@ public final class StorableGenerator { * fully thread-safe. The Storable type itself may be an interface or a * class. If it is a class, then it must not be final, and it must have a * public, no-arg constructor. The constructor signature for the returned - * abstract is defined as follows: + * abstract class is defined as follows: * *

      * /**
@@ -569,27 +569,29 @@ public final class StorableGenerator {
             for (StorableProperty property : mAllProperties.values()) {
                 ordinal++;
 
-                if (property.isVersion()) {
+                if (!property.isDerived() && property.isVersion()) {
                     versionOrdinal = ordinal;
                 }
 
                 final String name = property.getName();
                 final TypeDesc type = TypeDesc.forClass(property.getType());
 
-                if (property.isJoin()) {
-                    // If generating wrapper, property access is not guarded by
-                    // synchronization. Mark as volatile instead.
-                    mClassFile.addField(Modifiers.PRIVATE.toVolatile(mGenMode == GEN_WRAPPED),
-                                        name, type);
-                    requireStateField = true;
-                } else if (mGenMode == GEN_ABSTRACT) {
-                    // Only define regular property fields if abstract
-                    // class. Wrapped class doesn't reference them. Double
-                    // words are volatile to prevent word tearing without
-                    // explicit synchronization.
-                    mClassFile.addField(Modifiers.PROTECTED.toVolatile(type.isDoubleWord()),
-                                        name, type);
-                    requireStateField = true;
+                if (!property.isDerived()) {
+                    if (property.isJoin()) {
+                        // If generating wrapper, property access is not guarded by
+                        // synchronization. Mark as volatile instead.
+                        mClassFile.addField(Modifiers.PRIVATE.toVolatile(mGenMode == GEN_WRAPPED),
+                                            name, type);
+                        requireStateField = true;
+                    } else if (mGenMode == GEN_ABSTRACT) {
+                        // Only define regular property fields if abstract
+                        // class. Wrapped class doesn't reference them. Double
+                        // words are volatile to prevent word tearing without
+                        // explicit synchronization.
+                        mClassFile.addField(Modifiers.PROTECTED.toVolatile(type.isDoubleWord()),
+                                            name, type);
+                        requireStateField = true;
+                    }
                 }
 
                 final String stateFieldName = PROPERTY_STATE_FIELD_NAME + (ordinal >> 4);
@@ -605,7 +607,7 @@ public final class StorableGenerator {
                 }
 
                 // Add read method.
-                buildReadMethod: {
+                buildReadMethod: if (!property.isDerived()) {
                     Method readMethod = property.getReadMethod();
 
                     MethodInfo mi;
@@ -849,7 +851,7 @@ public final class StorableGenerator {
                 }
 
                 // Add write method.
-                if (!property.isQuery()) {
+                buildWriteMethod: if (!property.isDerived() && !property.isQuery()) {
                     Method writeMethod = property.getWriteMethod();
 
                     MethodInfo mi;
@@ -1901,7 +1903,8 @@ public final class StorableGenerator {
                     new HashMap>();
 
                 for (StorableProperty property : mAllProperties.values()) {
-                    if (!property.isPrimaryKeyMember() &&
+                    if (!property.isDerived() &&
+                        !property.isPrimaryKeyMember() &&
                         !property.isJoin() &&
                         !property.isNullable()) {
 
@@ -2111,7 +2114,7 @@ public final class StorableGenerator {
 
         for (StorableProperty property : mAllProperties.values()) {
             // Decide if property should be part of the copy.
-            boolean shouldCopy = !property.isJoin() &&
+            boolean shouldCopy = !property.isDerived() && !property.isJoin() &&
                 (property.isPrimaryKeyMember() && pkProperties ||
                  property.isVersion() && versionProperty ||
                  !property.isPrimaryKeyMember() && dataProperties);
@@ -2309,7 +2312,7 @@ public final class StorableGenerator {
         int ordinal = 0;
         int mask = 0;
         for (StorableProperty property : mAllProperties.values()) {
-            if (property != joinProperty && !property.isJoin()) {
+            if (property != joinProperty && !property.isDerived() && !property.isJoin()) {
                 // Check to see if property is an internal member of joinProperty.
                 for (int i=joinProperty.getJoinElementCount(); --i>=0; ) {
                     if (property == joinProperty.getInternalJoinElement(i)) {
@@ -2343,19 +2346,21 @@ public final class StorableGenerator {
         for (StorableProperty property : mAllProperties.values()) {
             ordinal++;
 
-            if (property.isJoin() || mGenMode == GEN_ABSTRACT) {
-                requireStateField = true;
-            }
+            if (!property.isDerived()) {
+                if (property.isJoin() || mGenMode == GEN_ABSTRACT) {
+                    requireStateField = true;
+                }
 
-            if (ordinal == maxOrdinal || ((ordinal & 0xf) == 0xf)) {
-                if (requireStateField) {
-                    String stateFieldName = PROPERTY_STATE_FIELD_NAME + (ordinal >> 4);
+                if (ordinal == maxOrdinal || ((ordinal & 0xf) == 0xf)) {
+                    if (requireStateField) {
+                        String stateFieldName = PROPERTY_STATE_FIELD_NAME + (ordinal >> 4);
 
-                    b.loadThis();
-                    b.loadConstant(0);
-                    b.storeField(stateFieldName, TypeDesc.INT);
+                        b.loadThis();
+                        b.loadConstant(0);
+                        b.storeField(stateFieldName, TypeDesc.INT);
+                    }
+                    requireStateField = false;
                 }
-                requireStateField = false;
             }
         }
     }
@@ -2382,17 +2387,19 @@ public final class StorableGenerator {
         int orMask = 0;
 
         for (StorableProperty property : mAllProperties.values()) {
-            if (property.isQuery()) {
-                // Don't erase cached query.
-                andMask |= PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2);
-            } else if (!property.isJoin()) {
-                if (name == MARK_ALL_PROPERTIES_CLEAN) {
-                    // Force clean state (1) always.
-                    orMask |= PROPERTY_STATE_CLEAN << ((ordinal & 0xf) * 2);
-                } else if (name == MARK_PROPERTIES_CLEAN) {
-                    // Mask will convert dirty (3) to clean (1). State 2, which
-                    // is illegal, is converted to 0.
-                    andMask |= PROPERTY_STATE_CLEAN << ((ordinal & 0xf) * 2);
+            if (!property.isDerived()) {
+                if (property.isQuery()) {
+                    // Don't erase cached query.
+                    andMask |= PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2);
+                } else if (!property.isJoin()) {
+                    if (name == MARK_ALL_PROPERTIES_CLEAN) {
+                        // Force clean state (1) always.
+                        orMask |= PROPERTY_STATE_CLEAN << ((ordinal & 0xf) * 2);
+                    } else if (name == MARK_PROPERTIES_CLEAN) {
+                        // Mask will convert dirty (3) to clean (1). State 2, which
+                        // is illegal, is converted to 0.
+                        andMask |= PROPERTY_STATE_CLEAN << ((ordinal & 0xf) * 2);
+                    }
                 }
             }
 
@@ -2443,14 +2450,16 @@ public final class StorableGenerator {
         int orMask = 0;
 
         for (StorableProperty property : mAllProperties.values()) {
-            if (property.isJoin()) {
-                // Erase cached join properties, but don't erase cached query.
-                if (!property.isQuery()) {
-                    andMask |= PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2);
+            if (!property.isDerived()) {
+                if (property.isJoin()) {
+                    // Erase cached join properties, but don't erase cached query.
+                    if (!property.isQuery()) {
+                        andMask |= PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2);
+                    }
+                } else if (name == MARK_ALL_PROPERTIES_DIRTY) {
+                    // Force dirty state (3).
+                    orMask |= PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2);
                 }
-            } else if (name == MARK_ALL_PROPERTIES_DIRTY) {
-                // Force dirty state (3).
-                orMask |= PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2);
             }
 
             ordinal++;
@@ -2514,16 +2523,18 @@ public final class StorableGenerator {
         int andMask = 0xffffffff;
         int orMask = 0;
         for (StorableProperty property : mAllProperties.values()) {
-            if (property == ordinaryProperty) {
-                if (mGenMode == GEN_ABSTRACT) {
-                    // Only GEN_ABSTRACT mode uses these state bits.
-                    orMask |= PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2);
-                }
-            } else if (property.isJoin()) {
-                // Check to see if ordinary is an internal member of join property.
-                for (int i=property.getJoinElementCount(); --i>=0; ) {
-                    if (ordinaryProperty == property.getInternalJoinElement(i)) {
-                        andMask &= ~(PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2));
+            if (!property.isDerived()) {
+                if (property == ordinaryProperty) {
+                    if (mGenMode == GEN_ABSTRACT) {
+                        // Only GEN_ABSTRACT mode uses these state bits.
+                        orMask |= PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2);
+                    }
+                } else if (property.isJoin()) {
+                    // Check to see if ordinary is an internal member of join property.
+                    for (int i=property.getJoinElementCount(); --i>=0; ) {
+                        if (ordinaryProperty == property.getInternalJoinElement(i)) {
+                            andMask &= ~(PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2));
+                        }
                     }
                 }
             }
@@ -2556,13 +2567,15 @@ public final class StorableGenerator {
         int ordinal = 0;
         int andMask = 0;
         for (StorableProperty property : mAllProperties.values()) {
-            if (!property.isJoin() && (!property.isPrimaryKeyMember() || includePk)) {
-                // Logical 'and' will convert state 1 (clean) to state 0, so
-                // that it will be ignored. State 3 (dirty) is what we're
-                // looking for, and it turns into 2. Essentially, we leave the
-                // high order bit on, since there is no state which has the
-                // high order bit on unless the low order bit is also on.
-                andMask |= 2 << ((ordinal & 0xf) * 2);
+            if (!property.isDerived()) {
+                if (!property.isJoin() && (!property.isPrimaryKeyMember() || includePk)) {
+                    // Logical 'and' will convert state 1 (clean) to state 0, so
+                    // that it will be ignored. State 3 (dirty) is what we're
+                    // looking for, and it turns into 2. Essentially, we leave the
+                    // high order bit on, since there is no state which has the
+                    // high order bit on unless the low order bit is also on.
+                    andMask |= 2 << ((ordinal & 0xf) * 2);
+                }
             }
             ordinal++;
             if ((ordinal & 0xf) == 0 || ordinal >= count) {
@@ -2623,8 +2636,10 @@ public final class StorableGenerator {
         int ordinal = 0;
         int mask = 0;
         for (StorableProperty property : mAllProperties.values()) {
-            if (properties.containsKey(property.getName())) {
-                mask |= PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2);
+            if (!property.isDerived()) {
+                if (properties.containsKey(property.getName())) {
+                    mask |= PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2);
+                }
             }
             ordinal++;
             if (((ordinal & 0xf) == 0 || ordinal >= mAllProperties.size()) && mask != 0) {
@@ -2772,6 +2787,7 @@ public final class StorableGenerator {
         // Params to invoke String.equals.
         TypeDesc[] params = {TypeDesc.OBJECT};
 
+        Label derivedMatch = null;
         Label joinMatch = null;
 
         for (int i=0; i {
                     b.ifZeroComparisonBranch(notEqual, "==");
                 }
 
-                if (prop.isJoin()) {
+                if (prop.isDerived()) {
+                    if (derivedMatch == null) {
+                        derivedMatch = b.createLabel();
+                    }
+                    b.branch(derivedMatch);
+                } else if (prop.isJoin()) {
                     if (joinMatch == null) {
                         joinMatch = b.createLabel();
                     }
@@ -2841,6 +2862,18 @@ public final class StorableGenerator {
         b.invokeConstructor(exceptionType, params);
         b.throwObject();
 
+        if (derivedMatch != null) {
+            derivedMatch.setLocation();
+
+            b.newObject(exceptionType);
+            b.dup();
+            b.loadConstant("Cannot get state for derived property: ");
+            b.loadLocal(b.getParameter(0));
+            b.invokeVirtual(TypeDesc.STRING, "concat", TypeDesc.STRING, params);
+            b.invokeConstructor(exceptionType, params);
+            b.throwObject();
+        }
+
         if (joinMatch != null) {
             joinMatch.setLocation();
 
@@ -2936,7 +2969,7 @@ public final class StorableGenerator {
 
         boolean mixIn = false;
         for (StorableProperty property : mAllProperties.values()) {
-            if (property.isJoin()) {
+            if (property.isDerived() || property.isJoin()) {
                 continue;
             }
             addHashCodeCall(b, property.getName(),
@@ -3105,7 +3138,7 @@ public final class StorableGenerator {
         b.storeLocal(other);
 
         for (StorableProperty property : mAllProperties.values()) {
-            if (property.isJoin()) {
+            if (property.isDerived() || property.isJoin()) {
                 continue;
             }
             // If we're only comparing keys, and this isn't a key, skip it
@@ -3211,8 +3244,10 @@ public final class StorableGenerator {
         // Second pass, print non-primary keys.
         if (!keyOnly) {
             for (StorableProperty property : mAllProperties.values()) {
-                // Don't print join properties if they may throw an exception.
-                if (!property.isPrimaryKeyMember() && (!property.isJoin())) {
+                // Don't print any derived or join properties since they may throw an exception.
+                if (!property.isPrimaryKeyMember() &&
+                    (!property.isDerived()) && (!property.isJoin()))
+                {
                     Label skipPrint = b.createLabel();
 
                     // Check if independent property is supported, and skip if not.
diff --git a/src/main/java/com/amazon/carbonado/gen/StorableSerializer.java b/src/main/java/com/amazon/carbonado/gen/StorableSerializer.java
index 5115854..09eff96 100644
--- a/src/main/java/com/amazon/carbonado/gen/StorableSerializer.java
+++ b/src/main/java/com/amazon/carbonado/gen/StorableSerializer.java
@@ -116,11 +116,11 @@ public abstract class StorableSerializer {
 
         StorableProperty[] properties;
         {
-            // Exclude joins.
+            // Exclude derived properties and joins.
             List> list =
                 new ArrayList>(propertyMap.size());
             for (StorableProperty property : propertyMap.values()) {
-                if (!property.isJoin()) {
+                if (!property.isDerived() && !property.isJoin()) {
                     list.add(property);
                 }
             }
diff --git a/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java b/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java
index f318c33..7451fbd 100644
--- a/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java
+++ b/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java
@@ -51,6 +51,7 @@ import com.amazon.carbonado.Alias;
 import com.amazon.carbonado.AlternateKeys;
 import com.amazon.carbonado.Authoritative;
 import com.amazon.carbonado.Automatic;
+import com.amazon.carbonado.Derived;
 import com.amazon.carbonado.FetchException;
 import com.amazon.carbonado.Index;
 import com.amazon.carbonado.Indexes;
@@ -230,9 +231,54 @@ public class StorableIntrospector {
             // late, then there would be a stack overflow.
             for (StorableProperty property : properties.values()) {
                 if (property instanceof JoinProperty) {
-                    ((JoinProperty)property).resolve(errorMessages, properties);
+                    ((JoinProperty)property).resolveJoin(errorMessages, info);
                 }
             }
+
+            // Resolve derived properties after join properties, since they may
+            // depend on them.
+            boolean anyDerived = false;
+            for (StorableProperty property : properties.values()) {
+                if (property instanceof SimpleProperty && property.isDerived()) {
+                    anyDerived = true;
+                    ((SimpleProperty)property).resolveDerivedFrom(errorMessages, info);
+                }
+            }
+
+            if (anyDerived && errorMessages.size() == 0) {
+                // Make sure that any indexes which refer to derived properties
+                // throwing FetchException have derived-from properties
+                // listed. Why? The exception likely indicates that a join
+                // property is being fetched.
+
+                for (StorableIndex index : indexes) {
+                    for (StorableProperty property : index.getProperties()) {
+                        if (property.isDerived() && property.getReadMethod() != null &&
+                            property.getDerivedFromProperties().length == 0)
+                        {
+                            Class exceptionType = FetchException.class;
+
+                            Class[] exceptions = property.getReadMethod().getExceptionTypes();
+                            boolean fetches = false;
+                            for (int i=exceptions.length; --i>=0; ) {
+                                if (exceptions[i].isAssignableFrom(exceptionType)) {
+                                    fetches = true;
+                                    break;
+                                }
+                            }
+
+                            if (fetches) {
+                                errorMessages.add
+                                    ("Index refers to a derived property which declares " +
+                                     "throwing a FetchException, but property does not " +
+                                     "list any derived-from properties: \"" +
+                                     property.getName() + "'");
+                            }
+                        }
+                    }
+                }
+            }
+
             if (errorMessages.size() > 0) {
                 cCache.remove(type);
                 throw new MalformedTypeException(type, errorMessages);
@@ -482,14 +528,15 @@ public class StorableIntrospector {
             }
             // Check if abstract method is just redefining a method in
             // Storable.
-            // TODO: Check if abstract method is redefining return type, which
-            // is allowed for copy method. The return type must be within its
-            // bounds.
             try {
                 Method m2 = Storable.class.getMethod(m.getName(), (Class[]) m.getParameterTypes());
                 if (m.getReturnType() == m2.getReturnType()) {
                     it.remove();
                 }
+                // Copy method can be redefined with specialized return type.
+                if (m.getName().equals("copy") && type.isAssignableFrom(m.getReturnType())) {
+                    it.remove();
+                }
             } catch (NoSuchMethodException e) {
                 // Not defined in Storable.
             }
@@ -523,7 +570,20 @@ public class StorableIntrospector {
             Method readMethod = property.getReadMethod();
             Method writeMethod = property.getWriteMethod();
 
-            if (readMethod == null && writeMethod == null) {
+            boolean isAbstract;
+            if (readMethod == null) {
+                if (writeMethod == null) {
+                    continue;
+                } else if (!Modifier.isAbstract(writeMethod.getModifiers()) &&
+                           writeMethod.getAnnotation(Derived.class) == null)
+                {
+                    // Ignore concrete property methods unless they're derived.
+                    continue;
+                }
+            } else if (!Modifier.isAbstract(readMethod.getModifiers()) &&
+                       readMethod.getAnnotation(Derived.class) == null)
+            {
+                // Ignore concrete property methods unless they're derived.
                 continue;
             }
 
@@ -540,7 +600,7 @@ public class StorableIntrospector {
 
             if (readMethod != null) {
                 String sig = createSig(readMethod);
-                if (methods.containsKey(sig)) {
+                if (storableProp.isDerived() || methods.containsKey(sig)) {
                     methods.remove(sig);
                     properties.put(property.getName(), storableProp);
                 } else {
@@ -550,7 +610,7 @@ public class StorableIntrospector {
 
             if (writeMethod != null) {
                 String sig = createSig(writeMethod);
-                if (methods.containsKey(sig)) {
+                if (storableProp.isDerived() || methods.containsKey(sig)) {
                     methods.remove(sig);
                     properties.put(property.getName(), storableProp);
                 } else {
@@ -698,6 +758,7 @@ public class StorableIntrospector {
         Automatic automatic = null;
         Independent independent = null;
         Join join = null;
+        Derived derived = null;
 
         Method readMethod = property.getReadMethod();
         Method writeMethod = property.getWriteMethod();
@@ -717,14 +778,16 @@ public class StorableIntrospector {
             automatic = readMethod.getAnnotation(Automatic.class);
             independent = readMethod.getAnnotation(Independent.class);
             join = readMethod.getAnnotation(Join.class);
+            derived = readMethod.getAnnotation(Derived.class);
         }
 
         if (writeMethod == null) {
             if (readMethod == null || Modifier.isAbstract(readMethod.getModifiers())) {
                 // Set method is always required for non-join properties. More
                 // work is done later on join properties, and sometimes the
-                // write method is required.
-                if (join == null) {
+                // write method is required. Derived properties don't need a
+                // set method.
+                if (join == null && derived == null) {
                     errorMessages.add("Must define proper 'set' method for property: " +
                                       property.getName());
                 }
@@ -758,6 +821,35 @@ public class StorableIntrospector {
                 errorMessages.add
                     ("Join annotation not allowed on mutator: " + writeMethod);
             }
+            if (writeMethod.getAnnotation(Derived.class) != null) {
+                errorMessages.add
+                    ("Derived annotation not allowed on mutator: " + writeMethod);
+            }
+        }
+
+        if (derived != null) {
+            if (readMethod != null && Modifier.isAbstract(readMethod.getModifiers()) ||
+                writeMethod != null && Modifier.isAbstract(writeMethod.getModifiers()))
+            {
+                errorMessages.add("Derived properties cannot be abstract: " +
+                                  property.getName());
+            }
+            if (pk) {
+                errorMessages.add("Derived properties cannot be a member of primary key: " +
+                                  property.getName());
+            }
+            if (sequence != null) {
+                errorMessages.add("Derived properties cannot have a Sequence annotation: " +
+                                  property.getName());
+            }
+            if (automatic != null) {
+                errorMessages.add("Derived properties cannot have an Automatic annotation: " +
+                                  property.getName());
+            }
+            if (join != null) {
+                errorMessages.add("Derived properties cannot have a Join annotation: " +
+                                  property.getName());
+            }
         }
 
         if (nullable != null && property.getType().isPrimitive()) {
@@ -813,6 +905,48 @@ public class StorableIntrospector {
             gatherAdapters(property, writeMethod, false, errorMessages);
         }
 
+        // Check that declared checked exceptions are allowed.
+        if (readMethod != null) {
+            for (Class ex : readMethod.getExceptionTypes()) {
+                if (RuntimeException.class.isAssignableFrom(ex)
+                    || Error.class.isAssignableFrom(ex))
+                {
+                    continue;
+                }
+                if (join != null || derived != null) {
+                    if (FetchException.class.isAssignableFrom(ex)) {
+                        continue;
+                    }
+                    errorMessages.add
+                        ("Checked exceptions thrown by join or derived property accessors " +
+                         "must be of type FetchException: \"" + readMethod.getName() +
+                         "\" declares throwing \"" + ex.getName() + '"');
+                    break;
+                } else {
+                    errorMessages.add
+                        ("Only join and derived property accessors can throw checked " +
+                         "exceptions: \"" + readMethod.getName() + "\" declares throwing \"" +
+                         ex.getName() + '"');
+                    break;
+                }
+            }
+        }
+
+        // Check that declared checked exceptions are allowed.
+        if (writeMethod != null) {
+            for (Class ex : writeMethod.getExceptionTypes()) {
+                if (RuntimeException.class.isAssignableFrom(ex)
+                    || Error.class.isAssignableFrom(ex))
+                {
+                    continue;
+                }
+                errorMessages.add
+                    ("Mutators cannot throw checked exceptions: \"" + writeMethod.getName() +
+                     "\" declares throwing \"" + ex.getName() + '"');
+                break;
+            }
+        }
+
         String sequenceName = null;
         if (sequence != null) {
             sequenceName = sequence.value();
@@ -825,7 +959,8 @@ public class StorableIntrospector {
             return new SimpleProperty
                 (property, enclosing, nullable != null, pk, altKey,
                  aliases, constraints, adapters == null ? null : adapters[0],
-                 version != null, sequenceName, independent != null, automatic != null);
+                 version != null, sequenceName,
+                 independent != null, automatic != null, derived);
         }
 
         // Do additional work for join properties.
@@ -933,7 +1068,7 @@ public class StorableIntrospector {
         return new JoinProperty
             (property, enclosing, nullable != null, aliases,
              constraints, adapters == null ? null : adapters[0],
-             sequenceName, independent != null, automatic != null,
+             sequenceName, independent != null, automatic != null, derived,
              joinedType, internal, external);
     }
 
@@ -1412,6 +1547,8 @@ public class StorableIntrospector {
     }
 
     private static class SimpleProperty implements StorableProperty {
+        private static final ChainedProperty[] EMPTY_CHAIN_ARRAY = new ChainedProperty[0];
+
         private final BeanProperty mBeanProperty;
         private final Class mEnclosingType;
         private final boolean mNullable;
@@ -1424,13 +1561,24 @@ public class StorableIntrospector {
         private final String mSequence;
         private final boolean mIndependent;
         private final boolean mAutomatic;
+        private final boolean mIsDerived;
+
+        // Temporary reference until derived from is resolved.
+        private Derived mDerived;
+
+        // Resolved derived from properties.
+        private ChainedProperty[] mDerivedFrom;
+
+        // Resolved derived to properties.
+        private ChainedProperty[] mDerivedTo;
 
         SimpleProperty(BeanProperty property, Class enclosing,
                        boolean nullable, boolean primaryKey, boolean alternateKey,
                        String[] aliases, StorablePropertyConstraint[] constraints,
                        StorablePropertyAdapter adapter,
                        boolean isVersion, String sequence,
-                       boolean independent, boolean automatic)
+                       boolean independent, boolean automatic,
+                       Derived derived)
         {
             mBeanProperty = property;
             mEnclosingType = enclosing;
@@ -1444,6 +1592,8 @@ public class StorableIntrospector {
             mSequence = sequence;
             mIndependent = independent;
             mAutomatic = automatic;
+            mIsDerived = derived != null;
+            mDerived = derived;
         }
 
         public final String getName() {
@@ -1536,10 +1686,45 @@ public class StorableIntrospector {
             return mIsVersion;
         }
 
+        public final boolean isDerived() {
+            return mIsDerived;
+        }
+
+        public final ChainedProperty[] getDerivedFromProperties() {
+            return (!mIsDerived || mDerivedFrom == null) ?
+                EMPTY_CHAIN_ARRAY : mDerivedFrom.clone();
+        }
+
+        public final ChainedProperty[] getDerivedToProperties() {
+            if (mDerivedTo == null) {
+                // Derived-to properties must be determined on demand because
+                // introspection might have been initiated by a dependency. If
+                // that dependency is asked for derived properties, it will not
+                // yet have resolved derived-from properties.
+
+                Set> derivedToSet = new LinkedHashSet>();
+                Set> examinedSet = new HashSet>();
+
+                addToDerivedToSet(derivedToSet, examinedSet, examine(getEnclosingType()));
+
+                if (derivedToSet.size() > 0) {
+                    mDerivedTo = derivedToSet.toArray(new ChainedProperty[derivedToSet.size()]);
+                } else {
+                    mDerivedTo = EMPTY_CHAIN_ARRAY;
+                }
+            }
+
+            return mDerivedTo.clone();
+        }
+
         public boolean isJoin() {
             return false;
         }
 
+        public boolean isOneToOneJoin() {
+            return false;
+        }
+
         public Class getJoinedType() {
             return null;
         }
@@ -1635,6 +1820,216 @@ public class StorableIntrospector {
             app.append(getEnclosingType().getName());
             app.append('}');
         }
+
+        void resolveDerivedFrom(List errorMessages, StorableInfo info) {
+            Derived derived = mDerived;
+            // Don't need this anymore.
+            mDerived = null;
+
+            if (!mIsDerived || derived == null) {
+                return;
+            }
+            String[] fromNames = derived.from();
+            if (fromNames == null || fromNames.length == 0) {
+                return;
+            }
+
+            Set> derivedFromSet = new LinkedHashSet>();
+
+            for (String fromName : fromNames) {
+                ChainedProperty from;
+                try {
+                    from = ChainedProperty.parse(info, fromName);
+                } catch (IllegalArgumentException e) {
+                    errorMessages.add
+                        ("Cannot find derived-from property: \"" +
+                         getName() + "\" reports being derived from \"" +
+                         fromName + '"');
+                    continue;
+                }
+                addToDerivedFromSet(errorMessages, derivedFromSet, from);
+            }
+
+            if (derivedFromSet.size() > 0) {
+                if (derivedFromSet.contains(ChainedProperty.get(this))) {
+                    errorMessages.add
+                        ("Derived-from dependency cycle detected: \"" + getName() + '"');
+                }
+
+                mDerivedFrom = derivedFromSet
+                    .toArray(new ChainedProperty[derivedFromSet.size()]);
+            } else {
+                mDerivedFrom = null;
+            }
+        }
+
+        private boolean addToDerivedFromSet(List errorMessages,
+                                            Set> derivedFromSet,
+                                            ChainedProperty from)
+        {
+            if (derivedFromSet.contains(from)) {
+                return false;
+            }
+
+            derivedFromSet.add(from);
+
+            ChainedProperty trimmed = from.getChainCount() == 0 ? null : from.trim();
+
+            if (trimmed != null) {
+                // Include all join properties as dependencies.
+                addToDerivedFromSet(errorMessages, derivedFromSet, trimmed);
+            }
+
+            StorableProperty lastInChain = from.getLastProperty();
+
+            if (lastInChain.isDerived()) {
+                // Expand derived dependencies.
+                ((SimpleProperty) lastInChain)
+                    .resolveDerivedFrom(errorMessages, examine(lastInChain.getEnclosingType()));
+                for (ChainedProperty lastFrom : lastInChain.getDerivedFromProperties()) {
+                    ChainedProperty dep;
+                    if (trimmed == null) {
+                        dep = (ChainedProperty) lastFrom;
+                    } else {
+                        dep = trimmed.append(lastFrom);
+                    }
+                    addToDerivedFromSet(errorMessages, derivedFromSet, dep);
+                }
+            }
+
+            if (lastInChain.isJoin() && errorMessages.size() == 0) {
+                // Make sure that join is doubly specified. Why? Consider the
+                // case where the derived property is a member of an index or
+                // key. If the joined Storable class gets loaded first, it will
+                // not know that an index exists that it should keep
+                // up-to-date. With the double join, it can check to see if
+                // there are any foreign indexes. This check could probably be
+                // skipped if the derived property doesn't belong to an index
+                // or key, but consistent error checking behavior is desirable.
+
+                Class joined = lastInChain.getJoinedType();
+
+                doubly: {
+                    for (StorableProperty prop : examine(joined).getAllProperties().values()) {
+                        if (prop.isJoin() &&
+                            prop.getJoinedType() == lastInChain.getEnclosingType())
+                        {
+                            break doubly;
+                        }
+                    }
+
+                    StringBuilder suggest = new StringBuilder();
+
+                    suggest.append("@Join");
+
+                    int count = lastInChain.getJoinElementCount();
+                    boolean naturalJoin = true;
+                    for (int i=0; i 1) {
+                            suggest.append('}');
+                        }
+
+                        suggest.append(", external=");
+                        if (count > 1) {
+                            suggest.append('{');
+                        }
+                        for (int i=0; i 0) {
+                                suggest.append(", ");
+                            }
+                            suggest.append('"');
+                            // This property's internal is other's external.
+                            suggest.append(lastInChain.getInternalJoinElement(i).getName());
+                            suggest.append('"');
+                        }
+                        if (count > 1) {
+                            suggest.append('}');
+                        }
+
+                        suggest.append(")");
+                    }
+
+                    suggest.append(' ');
+
+                    if (!joined.isInterface()) {
+                        suggest.append("public abstract ");
+                    }
+
+                    if (lastInChain.isOneToOneJoin() || lastInChain.isQuery()) {
+                        suggest.append(lastInChain.getEnclosingType().getName());
+                    } else {
+                        suggest.append("Query<");
+                        suggest.append(lastInChain.getEnclosingType().getName());
+                        suggest.append('>');
+                    }
+
+                    suggest.append(" getXxx() throws FetchException");
+
+                    errorMessages.add
+                        ("Derived-from property is a join, but it is not doubly joined: \"" +
+                         getName() + "\" is derived from \"" + from +
+                         "\". Consider defining a join property in " + joined + " as: " + suggest);
+                }
+            }
+
+            return true;
+        }
+
+        private boolean addToDerivedToSet(Set> derivedToSet,
+                                          Set> examinedSet,
+                                          StorableInfo info)
+        {
+            if (examinedSet.contains(info.getStorableType())) {
+                return false;
+            }
+
+            // Prevent infinite loop while following join paths.
+            examinedSet.add(info.getStorableType());
+
+            final int originalSize = derivedToSet.size();
+
+            for (StorableProperty property : info.getAllProperties().values()) {
+                if (property.isDerived()) {
+                    for (ChainedProperty from : property.getDerivedFromProperties()) {
+                        if (from.getLastProperty().equals(this)) {
+                            ChainedProperty path = ChainedProperty.get(property);
+                            if (from.getChainCount() > 0) {
+                                path = path.append(from.trim());
+                            }
+                            derivedToSet.add(path);
+                        }
+                    }
+                }
+                if (property.isJoin()) {
+                    addToDerivedToSet(derivedToSet, examinedSet,
+                                      examine(property.getJoinedType()));
+                }
+            }
+
+            return derivedToSet.size() > originalSize;
+        }
     }
 
     private static final class JoinProperty extends SimpleProperty {
@@ -1649,16 +2044,19 @@ public class StorableIntrospector {
         private StorableProperty[] mInternal;
         private StorableProperty[] mExternal;
 
+        private boolean mOneToOne;
+
         JoinProperty(BeanProperty property, Class enclosing,
                      boolean nullable,
                      String[] aliases, StorablePropertyConstraint[] constraints,
                      StorablePropertyAdapter adapter,
                      String sequence, boolean independent, boolean automatic,
+                     Derived derived,
                      Class joinedType,
                      String[] internal, String[] external)
         {
             super(property, enclosing, nullable, false, false,
-                  aliases, constraints, adapter, false, sequence, independent, automatic);
+                  aliases, constraints, adapter, false, sequence, independent, automatic, derived);
             mJoinedType = joinedType;
 
             int length = internal.length;
@@ -1674,6 +2072,10 @@ public class StorableIntrospector {
             return true;
         }
 
+        public boolean isOneToOneJoin() {
+            return mOneToOne;
+        }
+
         public Class getJoinedType() {
             return mJoinedType;
         }
@@ -1706,32 +2108,39 @@ public class StorableIntrospector {
          * Finishes the definition of this join property. Can only be called once.
          */
         @SuppressWarnings("unchecked")
-        void resolve(List errorMessages, Map> properties) {
-            StorableInfo joinedInfo = examine(getJoinedType());
+        void resolveJoin(List errorMessages, StorableInfo info) {
+            StorableInfo joinedInfo;
+            try {
+                joinedInfo = examine(getJoinedType());
 
-            if (mInternalNames.length == 0) {
-                // Since no join elements specified, perform a natural join.
-                // If the joined type is a list, then the join elements are
-                // defined by this enclosing type's primary keys. Otherwise,
-                // they are defined by the joined type's primary keys.
+                if (mInternalNames.length == 0) {
+                    // Since no join elements specified, perform a natural join.
+                    // If the joined type is a list, then the join elements are
+                    // defined by this enclosing type's primary keys. Otherwise,
+                    // they are defined by the joined type's primary keys.
 
-                Map> primaryKeys;
+                    Map> primaryKeys;
 
-                if (isQuery()) {
-                    primaryKeys = examine(getEnclosingType()).getPrimaryKeyProperties();
-                } else {
-                    primaryKeys = joinedInfo.getPrimaryKeyProperties();
-                }
+                    if (isQuery()) {
+                        primaryKeys = examine(getEnclosingType()).getPrimaryKeyProperties();
+                    } else {
+                        primaryKeys = joinedInfo.getPrimaryKeyProperties();
+                    }
 
-                mInternalNames = new String[primaryKeys.size()];
-                mExternalNames = new String[primaryKeys.size()];
+                    mInternalNames = new String[primaryKeys.size()];
+                    mExternalNames = new String[primaryKeys.size()];
 
-                int i = 0;
-                for (String name : primaryKeys.keySet()) {
-                    mInternalNames[i] = name;
-                    mExternalNames[i] = name;
-                    i++;
+                    int i = 0;
+                    for (String name : primaryKeys.keySet()) {
+                        mInternalNames[i] = name;
+                        mExternalNames[i] = name;
+                        i++;
+                    }
                 }
+            } catch (MalformedTypeException e) {
+                mInternal = new StorableProperty[0];
+                mExternal = new StorableProperty[0];
+                throw e;
             }
 
             mInternal = new StorableProperty[mInternalNames.length];
@@ -1740,7 +2149,7 @@ public class StorableIntrospector {
             // Verify that internal properties exist and are not themselves joins.
             for (int i=0; i primaryKeys =
+            Set primaryKey =
                 new HashSet(joinedInfo.getPrimaryKeyProperties().values());
 
             // Remove external properties from the primary key set.
             for (int i=0; i');
                 }
+
+                // Determine if one-to-one join. If internal properties
+                // completely specify any key, then it is one-to-one.
+
+                boolean oneToOne = false;
+
+                oneToOneCheck: {
+                    Set internalPrimaryKey =
+                        new HashSet(info.getPrimaryKeyProperties().values());
+                
+                    for (int i=0; i altKey = new HashSet();
+
+                        for (OrderedProperty op : info.getAlternateKey(i).getProperties()) {
+                            ChainedProperty chained = op.getChainedProperty();
+                            if (chained.getChainCount() > 0) {
+                                // Funny alt key. Pretend it does not exist.
+                                continue altKeyScan;
+                            }
+                            altKey.add(chained.getPrimeProperty());
+                        }
+
+                        for (int j=0; j extends Appender {
     String[] getAliases();
 
     /**
-     * Returns true if this property is joined to another Storable.
+     * Returns true if this property is joined in any way to another Storable.
      *
      * @see com.amazon.carbonado.Join
      */
     boolean isJoin();
 
+    /**
+     * Returns true if this property is one-to-one joined to another Storable.
+     *
+     * @see com.amazon.carbonado.Join
+     */
+    boolean isOneToOneJoin();
+
     /**
      * Returns the type of property this is joined to, or null if not joined.
      */
@@ -211,5 +218,32 @@ public interface StorableProperty extends Appender {
      */
     boolean isIndependent();
 
+    /**
+     * Returns true if this property is derived.
+     *
+     * @see com.amazon.carbonado.Derived
+     */
+    boolean isDerived();
+
+    /**
+     * Returns a new array with all the derived-from properties, which is empty
+     * if this is not a derived property. Otherwise, the set is the transitive
+     * closure of all dependent properties. This set may include joins and
+     * other derived properties.
+     */
+    ChainedProperty[] getDerivedFromProperties();
+
+    /**
+     * Returns a new array with all the properties which are derived from this
+     * one. The set is the transitive closure of all derived properties which
+     * depend on this one.
+     *
+     * 

Each property in the set is represented as a chain, where the prime + * property is the actual dependent property, and the tail is the path to + * reach this property's enclosing type. If a derived property resides in + * the same enclosing type as this one, the chain count is zero. + */ + ChainedProperty[] getDerivedToProperties(); + String toString(); } diff --git a/src/main/java/com/amazon/carbonado/layout/Layout.java b/src/main/java/com/amazon/carbonado/layout/Layout.java index 1c6b63d..ca50172 100644 --- a/src/main/java/com/amazon/carbonado/layout/Layout.java +++ b/src/main/java/com/amazon/carbonado/layout/Layout.java @@ -172,7 +172,7 @@ public class Layout { List list = new ArrayList(properties.size()); int ordinal = 0; for (StorableProperty property : properties) { - if (property.isJoin()) { + if (property.isDerived() || property.isJoin()) { continue; } StoredLayoutProperty storedLayoutProperty = mLayoutFactory.mPropertyStorage.prepare(); diff --git a/src/main/java/com/amazon/carbonado/qe/AbstractQuery.java b/src/main/java/com/amazon/carbonado/qe/AbstractQuery.java index 436c2c2..7639348 100644 --- a/src/main/java/com/amazon/carbonado/qe/AbstractQuery.java +++ b/src/main/java/com/amazon/carbonado/qe/AbstractQuery.java @@ -123,4 +123,10 @@ public abstract class AbstractQuery implements Query, App } return b.toString(); } + + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(Object obj); } diff --git a/src/main/java/com/amazon/carbonado/qe/EmptyQuery.java b/src/main/java/com/amazon/carbonado/qe/EmptyQuery.java index 161ad90..d9969c5 100644 --- a/src/main/java/com/amazon/carbonado/qe/EmptyQuery.java +++ b/src/main/java/com/amazon/carbonado/qe/EmptyQuery.java @@ -273,6 +273,24 @@ public final class EmptyQuery extends AbstractQuery { return false; } + @Override + public int hashCode() { + return mFactory.hashCode() * 31 + mOrdering.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof EmptyQuery) { + EmptyQuery other = (EmptyQuery) obj; + return mFactory.equals(other.mFactory) + && mOrdering.equals(other.mOrdering); + } + return false; + } + private IllegalStateException error() { return new IllegalStateException("Query doesn't have any parameters"); } diff --git a/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java b/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java index cba6776..c0fa0af 100644 --- a/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java +++ b/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java @@ -19,7 +19,9 @@ package com.amazon.carbonado.raw; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Map; import org.cojen.classfile.CodeAssembler; @@ -397,14 +399,15 @@ public class GenericEncodingStrategy { Map> map = StorableIntrospector.examine(mType).getDataProperties(); - StorableProperty[] properties = new StorableProperty[map.size()]; + List> list = new ArrayList>(map.size()); - int ordinal = 0; for (StorableProperty property : map.values()) { - properties[ordinal++] = property; + if (!property.isDerived()) { + list.add(property); + } } - return properties; + return list.toArray(new StorableProperty[list.size()]); } protected StorablePropertyInfo checkSupport(StorableProperty property) diff --git a/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java b/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java index 43677f2..2532556 100644 --- a/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java +++ b/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java @@ -797,7 +797,7 @@ public class GenericStorableCodec implements StorableCodec { + private final IndexedRepository mRepository; + private final IndexEntryAccessor[] mIndexEntryAccessors; + private final Query mQuery; + private final String[] mJoinProperties; + private final BeanPropertyAccessor mPropertyAccessor; + + /** + * @param derivedTo special chained property from StorableProperty.getDerivedToProperties + */ + DependentStorableFetcher(IndexedRepository repository, + Class sType, ChainedProperty derivedTo) + throws RepositoryException + { + if (derivedTo.getChainCount() == 0) { + throw new IllegalArgumentException(); + } + if (derivedTo.getLastProperty().getType() != sType) { + throw new IllegalArgumentException(); + } + if (!derivedTo.getLastProperty().isJoin()) { + throw new IllegalArgumentException(); + } + + Class dType = derivedTo.getPrimeProperty().getEnclosingType(); + + // Find the indexes that contain the prime derivedTo property. + List> accessorList = new ArrayList>(); + for (IndexEntryAccessor acc : repository.getIndexEntryAccessors(dType)) { + for (String indexPropName : acc.getPropertyNames()) { + if (indexPropName.equals(derivedTo.getPrimeProperty().getName())) { + accessorList.add(acc); + break; + } + } + } + + if (accessorList.size() == 0) { + throw new SupportException + ("Unable to find index accessors for derived-to property: " + derivedTo + + ", enclosing type: " + dType); + } + + // Build a query on D joined to S. + + StorableProperty join = (StorableProperty) derivedTo.getLastProperty(); + + ChainedProperty base; + if (derivedTo.getChainCount() <= 1) { + base = null; + } else { + base = derivedTo.tail().trim(); + } + + int joinElementCount = join.getJoinElementCount(); + String[] joinProperties = new String[joinElementCount]; + + Filter dFilter = Filter.getOpenFilter(dType); + for (int i=0; i element = join.getInternalJoinElement(i); + joinProperties[i] = element.getName(); + if (base == null) { + dFilter = dFilter.and(element.getName(), RelOp.EQ); + } else { + dFilter = dFilter.and(base.append(element).toString(), RelOp.EQ); + } + } + + mRepository = repository; + mIndexEntryAccessors = accessorList.toArray(new IndexEntryAccessor[accessorList.size()]); + mQuery = repository.storageFor(dType).query(dFilter); + mJoinProperties = joinProperties; + mPropertyAccessor = BeanPropertyAccessor.forClass(sType); + } + + public Transaction enterTransaction() { + return mRepository.enterTransaction(); + } + + public Cursor fetchDependenentStorables(S storable) throws FetchException { + Query query = mQuery; + for (String property : mJoinProperties) { + query = query.with(mPropertyAccessor.getPropertyValue(storable, property)); + } + return query.fetch(); + } + + /** + * @return amount added to list + */ + public int createIndexEntries(D master, List indexEntries) { + IndexEntryAccessor[] accessors = mIndexEntryAccessors; + int length = accessors.length; + for (int i=0; i extends Trigger { + private final DependentStorableFetcher mFetcher; + + /** + * @param derivedTo special chained property from StorableProperty.getDerivedToProperties + */ + DerivedIndexesTrigger(IndexedRepository repository, + Class sType, ChainedProperty derivedTo) + throws RepositoryException + { + this(new DependentStorableFetcher(repository, sType, derivedTo)); + } + + DerivedIndexesTrigger(DependentStorableFetcher fetcher) { + mFetcher = fetcher; + } + + @Override + public Object beforeInsert(S storable) throws PersistException { + return createDependentIndexEntries(storable); + } + + @Override + public void afterInsert(S storable, Object state) throws PersistException { + updateValues(storable, state); + } + + @Override + public Object beforeUpdate(S storable) throws PersistException { + return createDependentIndexEntries(storable); + } + + @Override + public void afterUpdate(S storable, Object state) throws PersistException { + updateValues(storable, state); + } + + @Override + public Object beforeDelete(S storable) throws PersistException { + try { + if (storable.copy().tryLoad()) { + return createDependentIndexEntries(storable); + } + } catch (FetchException e) { + throw e.toPersistException(); + } + return null; + } + + @Override + public void afterDelete(S storable, Object state) throws PersistException { + updateValues(storable, state); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof DerivedIndexesTrigger) { + DerivedIndexesTrigger other = (DerivedIndexesTrigger) obj; + return mFetcher.equals(other.mFetcher); + } + return false; + } + + private List createDependentIndexEntries(S storable) throws PersistException { + List dependentIndexEntries = new ArrayList(); + createDependentIndexEntries(storable, dependentIndexEntries); + return dependentIndexEntries; + } + + private void createDependentIndexEntries(S storable, List dependentIndexEntries) + throws PersistException + { + try { + Transaction txn = mFetcher.enterTransaction(); + try { + // Make sure write lock is acquired when reading dependencies + // since they might be updated later. Locks are held after this + // transaction exits since it is nested in the trigger's transaction. + txn.setForUpdate(true); + + Cursor dependencies = mFetcher.fetchDependenentStorables(storable); + try { + while (dependencies.hasNext()) { + mFetcher.createIndexEntries(dependencies.next(), dependentIndexEntries); + } + } finally { + dependencies.close(); + } + } finally { + txn.exit(); + } + } catch (FetchException e) { + throw e.toPersistException(); + } + } + + private void updateValues(S storable, Object state) throws PersistException { + if (state == null) { + return; + } + + List oldIndexEntries = (List) state; + int size = oldIndexEntries.size(); + + List newIndexEntries = new ArrayList(size); + createDependentIndexEntries(storable, newIndexEntries); + + if (size != newIndexEntries.size()) { + // This is not expected to happen. + throw new PersistException("Amount of affected dependent indexes changed: " + + size + " != " + newIndexEntries.size()); + } + + for (int i=0; i StorableIndexSet gatherDesiredIndexes(StorableInfo info) { + StorableIndexSet indexSet = new StorableIndexSet(); + indexSet.addIndexes(info); + indexSet.addAlternateKeys(info); + + // If any join properties are used by indexed derived properties, make + // sure join internal properties are indexed. + + for (StorableProperty property : info.getAllProperties().values()) { + if (!isJoinAndUsedByIndexedDerivedProperty(property)) { + continue; + } + + // Internal properties of join need to be indexed. Check if a + // suitable index exists before defining a new one. + + Filter filter = Filter.getOpenFilter(info.getStorableType()); + for (int i=property.getJoinElementCount(); --i>=0; ) { + filter = filter.and(property.getInternalJoinElement(i).getName(), RelOp.EQ); + } + + for (int i=info.getIndexCount(); --i>=0; ) { + FilteringScore score = FilteringScore.evaluate(info.getIndex(i), filter); + if (score.getIdentityCount() == property.getJoinElementCount()) { + // Suitable index already exists. + continue; + } + } + + Direction[] directions = new Direction[property.getJoinElementCount()]; + Arrays.fill(directions, Direction.UNSPECIFIED); + + StorableIndex index = + new StorableIndex(property.getInternalJoinElements(), directions); + + indexSet.add(index); + } + + return indexSet; + } + + static boolean isUsedByIndex(StorableProperty property) { + StorableInfo info = StorableIntrospector.examine(property.getEnclosingType()); + for (int i=info.getIndexCount(); --i>=0; ) { + StorableIndex index = info.getIndex(i); + int propertyCount = index.getPropertyCount(); + for (int j=0; j property) { + if (property.isJoin()) { + for (ChainedProperty derivedTo : property.getDerivedToProperties()) { + if (isUsedByIndex(derivedTo.getPrimeProperty())) { + return true; + } + } + } + return false; + } + + /** + * Returns derived-to properties in external storables that are used by indexes. + * + * @return null if none + */ + static Set> gatherDerivedToDependencies(StorableInfo info) { + Set> set = null; + for (StorableProperty property : info.getAllProperties().values()) { + for (ChainedProperty derivedTo : property.getDerivedToProperties()) { + if (derivedTo.getChainCount() > 0 && isUsedByIndex(derivedTo.getPrimeProperty())) { + if (set == null) { + set = new HashSet>(); + } + set.add(derivedTo); + } + } + } + return set; + } +} diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessor.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessor.java index 0c1e6b8..5f27aa8 100644 --- a/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessor.java +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessor.java @@ -59,7 +59,7 @@ public interface IndexEntryAccessor extends IndexInfo { /** * Returns true if the properties of the given index entry match those * contained in the master, exluding any version property. This will always - * return true after a call to setAllProperties. + * return true after a call to copyFromMaster. * * @param indexEntry index entry whose properties will be tested * @param master source of property values diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepository.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepository.java index 097185a..6bea049 100644 --- a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepository.java +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepository.java @@ -86,7 +86,7 @@ class IndexedRepository implements Repository, if (Unindexed.class.isAssignableFrom(type)) { // Verify no indexes. - int indexCount = IndexedStorage + int indexCount = IndexAnalysis .gatherDesiredIndexes(StorableIntrospector.examine(type)).size(); if (indexCount > 0) { throw new MalformedTypeException @@ -152,7 +152,12 @@ class IndexedRepository implements Repository, getIndexEntryAccessors(Class storableType) throws RepositoryException { - return ((IndexedStorage) storageFor(storableType)).getIndexEntryAccessors(); + Storage storage = storageFor(storableType); + if (storage instanceof IndexedStorage) { + return ((IndexedStorage) storage).getIndexEntryAccessors(); + } else { + return new IndexEntryAccessor[0]; + } } public String[] getUserStorableTypeNames() throws RepositoryException { diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java index fe0cfe8..28a9d35 100644 --- a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -32,9 +33,11 @@ import com.amazon.carbonado.FetchException; import com.amazon.carbonado.IsolationLevel; import com.amazon.carbonado.PersistException; import com.amazon.carbonado.Query; +import com.amazon.carbonado.Repository; import com.amazon.carbonado.RepositoryException; import com.amazon.carbonado.Storable; import com.amazon.carbonado.Storage; +import com.amazon.carbonado.SupportException; import com.amazon.carbonado.Transaction; import com.amazon.carbonado.Trigger; import com.amazon.carbonado.capability.IndexInfo; @@ -45,6 +48,7 @@ import com.amazon.carbonado.cursor.MergeSortBuffer; import com.amazon.carbonado.filter.Filter; +import com.amazon.carbonado.info.ChainedProperty; import com.amazon.carbonado.info.Direction; import com.amazon.carbonado.info.StorableInfo; import com.amazon.carbonado.info.StorableIntrospector; @@ -53,6 +57,7 @@ import com.amazon.carbonado.info.StorableIndex; import com.amazon.carbonado.cursor.SortBuffer; import com.amazon.carbonado.qe.BoundaryType; +import com.amazon.carbonado.qe.FilteringScore; import com.amazon.carbonado.qe.QueryEngine; import com.amazon.carbonado.qe.QueryExecutorFactory; import com.amazon.carbonado.qe.StorageAccess; @@ -69,13 +74,6 @@ import static com.amazon.carbonado.repo.indexed.ManagedIndex.*; * @author Brian S O'Neill */ class IndexedStorage implements Storage, StorageAccess { - static StorableIndexSet gatherDesiredIndexes(StorableInfo info) { - StorableIndexSet indexSet = new StorableIndexSet(); - indexSet.addIndexes(info); - indexSet.addAlternateKeys(info); - return indexSet; - } - final IndexedRepository mRepository; final Storage mMasterStorage; @@ -102,7 +100,7 @@ class IndexedStorage implements Storage, StorageAccess // The set of indexes that the Storable defines, reduced. final StorableIndexSet desiredIndexSet; { - desiredIndexSet = gatherDesiredIndexes(info); + desiredIndexSet = IndexAnalysis.gatherDesiredIndexes(info); desiredIndexSet.reduce(Direction.ASCENDING); } @@ -299,6 +297,17 @@ class IndexedStorage implements Storage, StorageAccess mQueryableIndexSet = queryableIndexSet; mQueryEngine = new QueryEngine(masterStorage.getStorableType(), repository); + + // Install triggers to manage derived properties in external Storables. + + Set> derivedToDependencies = + IndexAnalysis.gatherDerivedToDependencies(info); + + if (derivedToDependencies != null) { + for (ChainedProperty derivedTo : derivedToDependencies) { + addTrigger(new DerivedIndexesTrigger(repository, getStorableType(), derivedTo)); + } + } } public Class getStorableType() { diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/ManagedIndex.java b/src/main/java/com/amazon/carbonado/repo/indexed/ManagedIndex.java index 2e4cea3..b1e76cc 100644 --- a/src/main/java/com/amazon/carbonado/repo/indexed/ManagedIndex.java +++ b/src/main/java/com/amazon/carbonado/repo/indexed/ManagedIndex.java @@ -18,6 +18,8 @@ package com.amazon.carbonado.repo.indexed; +import java.lang.reflect.UndeclaredThrowableException; + import java.util.Comparator; import org.apache.commons.logging.Log; @@ -451,10 +453,23 @@ class ManagedIndex implements IndexEntryAccessor { } } - private Storable makeIndexEntry(S userStorable) { - Storable indexEntry = mIndexEntryStorage.prepare(); - mGenerator.copyFromMaster(indexEntry, userStorable); - return indexEntry; + private Storable makeIndexEntry(S userStorable) throws PersistException { + try { + Storable indexEntry = mIndexEntryStorage.prepare(); + mGenerator.copyFromMaster(indexEntry, userStorable); + return indexEntry; + } catch (UndeclaredThrowableException e) { + Throwable cause = e.getCause(); + if (cause instanceof PersistException) { + throw (PersistException) cause; + } + throw new PersistException(cause); + } catch (Exception e) { + if (e instanceof PersistException) { + throw (PersistException) e; + } + throw new PersistException(e); + } } /** Assumes caller is in a transaction */ diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableGenerator.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableGenerator.java index 2528cbf..57f7bbc 100644 --- a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableGenerator.java +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableGenerator.java @@ -223,7 +223,7 @@ class JDBCStorableGenerator { // UnsupportedOperationException. { for (JDBCStorableProperty property : mAllProperties.values()) { - if (property.isJoin() || property.isSupported()) { + if (property.isDerived() || property.isJoin() || property.isSupported()) { continue; } String message = "Independent property \"" + property.getName() + @@ -444,7 +444,7 @@ class JDBCStorableGenerator { sb.append(" ( "); int ordinal = 0; - for (JDBCStorableProperty property : mInfo.getAllProperties().values()) { + for (JDBCStorableProperty property : mAllProperties.values()) { if (!property.isSelectable()) { continue; } @@ -470,10 +470,12 @@ class JDBCStorableGenerator { } boolean useStaticInsertStatement = true; - for (JDBCStorableProperty property : mInfo.getAllProperties().values()) { - if (property.isVersion() || property.isAutomatic()) { - useStaticInsertStatement = false; - break; + for (JDBCStorableProperty property : mAllProperties.values()) { + if (!property.isDerived()) { + if (property.isVersion() || property.isAutomatic()) { + useStaticInsertStatement = false; + break; + } } } @@ -489,7 +491,7 @@ class JDBCStorableGenerator { insertCountVar = b.createLocalVariable(null, TypeDesc.INT); int initialCount = 0; - for (JDBCStorableProperty property : mInfo.getAllProperties().values()) { + for (JDBCStorableProperty property : mAllProperties.values()) { if (!property.isSelectable()) { continue; } @@ -517,7 +519,7 @@ class JDBCStorableGenerator { CodeBuilderUtil.callStringBuilderAppendString(b); int propNumber = -1; - for (JDBCStorableProperty property : mInfo.getAllProperties().values()) { + for (JDBCStorableProperty property : mAllProperties.values()) { propNumber++; if (!property.isSelectable()) { continue; diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableIntrospector.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableIntrospector.java index aa17f11..5ff713d 100644 --- a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableIntrospector.java +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableIntrospector.java @@ -55,6 +55,7 @@ import com.amazon.carbonado.RepositoryException; import com.amazon.carbonado.Storable; import com.amazon.carbonado.SupportException; +import com.amazon.carbonado.info.ChainedProperty; import com.amazon.carbonado.info.OrderedProperty; import com.amazon.carbonado.info.StorableInfo; import com.amazon.carbonado.info.StorableIntrospector; @@ -277,7 +278,7 @@ public class JDBCStorableIntrospector extends StorableIntrospector { ArrayList errorMessages = new ArrayList(); for (StorableProperty mainProperty : mainProperties.values()) { - if (mainProperty.isJoin() || tableName == null) { + if (mainProperty.isDerived() || mainProperty.isJoin() || tableName == null) { jProperties.put(mainProperty.getName(), new JProperty(mainProperty)); continue; } @@ -1271,6 +1272,10 @@ public class JDBCStorableIntrospector extends StorableIntrospector { return mMainProperty.isJoin(); } + public boolean isOneToOneJoin() { + return mMainProperty.isOneToOneJoin(); + } + public Class getJoinedType() { return mMainProperty.getJoinedType(); } @@ -1315,6 +1320,18 @@ public class JDBCStorableIntrospector extends StorableIntrospector { return mMainProperty.isIndependent(); } + public boolean isDerived() { + return mMainProperty.isDerived(); + } + + public ChainedProperty[] getDerivedFromProperties() { + return mMainProperty.getDerivedFromProperties(); + } + + public ChainedProperty[] getDerivedToProperties() { + return mMainProperty.getDerivedToProperties(); + } + public boolean isSupported() { if (isJoin()) { // TODO: Check if joined type is supported @@ -1325,7 +1342,7 @@ public class JDBCStorableIntrospector extends StorableIntrospector { } public boolean isSelectable() { - return mColumnName != null && !isJoin(); + return mColumnName != null && !isJoin() && !isDerived(); } public boolean isAutoIncrement() { diff --git a/src/main/java/com/amazon/carbonado/spi/WrappedQuery.java b/src/main/java/com/amazon/carbonado/spi/WrappedQuery.java index ec3ae34..ba3c013 100644 --- a/src/main/java/com/amazon/carbonado/spi/WrappedQuery.java +++ b/src/main/java/com/amazon/carbonado/spi/WrappedQuery.java @@ -199,6 +199,23 @@ public abstract class WrappedQuery implements Query { return mQuery.toString(); } + @Override + public int hashCode() { + return mQuery.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof WrappedQuery) { + WrappedQuery other = (WrappedQuery) obj; + return mQuery.equals(other.mQuery); + } + return false; + } + protected Query getWrappedQuery() { return mQuery; } diff --git a/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceBuilder.java b/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceBuilder.java index 2c9c0dc..0832aa9 100644 --- a/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceBuilder.java +++ b/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceBuilder.java @@ -493,23 +493,22 @@ public class SyntheticStorableReferenceBuilder continue; } - if (prop.getReadMethod() == null) { - throw new SupportException - ("Property does not have a public accessor method: " + prop); - } - if (prop.getWriteMethod() == null) { - throw new SupportException - ("Property does not have a public mutator method: " + prop); - } - TypeDesc propType = TypeDesc.forClass(prop.getType()); if (toMasterPk) { + if (prop.getWriteMethod() == null) { + throw new SupportException + ("Property does not have a public mutator method: " + prop); + } b.loadLocal(b.getParameter(0)); b.loadThis(); b.invokeVirtual(prop.getReadMethodName(), propType, null); b.invoke(prop.getWriteMethod()); } else if (methodName.equals(mCopyFromMasterMethodName)) { + if (prop.getReadMethod() == null) { + throw new SupportException + ("Property does not have a public accessor method: " + prop); + } b.loadThis(); b.loadLocal(b.getParameter(0)); b.invoke(prop.getReadMethod()); -- cgit v1.2.3