From 97af4be638e371a2f693bde2798fc233a143f3f9 Mon Sep 17 00:00:00 2001
From: "Brian S. O'Neill" <bronee@gmail.com>
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

(limited to 'src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>Example:<pre>
+ * &#64;Indexes(&#64;Index("uppercaseName"))
+ * public abstract class UserInfo implements Storable&lt;UserInfo&gt; {
+ *     /**
+ *      * Derive an uppercase name for case-insensitive searches.
+ *      *&#47;
+ *     <b>&#64;Derived</b>
+ *     public String getUppercaseName() {
+ *         String name = getName();
+ *         return name == null ? null : name.toUpperCase();
+ *     }
+ *
+ *     ...
+ * }
+ * </pre>
+ *
+ * @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.
  *
- * <p>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.
+ * <p>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.
  *
- * <p><b>Manage</b>: 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.
- *
- * <p><b>Respect</b>: 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.
- *
- * <p><b>Check</b>: 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.
+ * <p>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.
  *
  * <p>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.
      *
      * <P>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<S extends Storable> {
         anySequences:
         if (features.contains(MasterFeature.INSERT_SEQUENCES)) {
             for (StorableProperty<S> property : info.getAllProperties().values()) {
-                if (property.getSequenceName() != null) {
+                if (!property.isDerived() && property.getSequenceName() != null) {
                     break anySequences;
                 }
             }
@@ -266,7 +266,7 @@ public final class MasterStorableGenerator<S extends Storable> {
 
                 int ordinal = 0;
                 for (StorableProperty<S> 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<S extends Storable> {
             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<S extends Storable> {
                 for (StorableProperty<S> 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<S extends Storable> {
                 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<S> 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<S extends Storable> {
                 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<S extends Storable> {
         }
     }
 
+    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<S extends Storable> {
      * 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:
      *
      * <pre>
      * /**
@@ -569,27 +569,29 @@ public final class StorableGenerator<S extends Storable> {
             for (StorableProperty<S> 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<S extends Storable> {
                 }
 
                 // Add read method.
-                buildReadMethod: {
+                buildReadMethod: if (!property.isDerived()) {
                     Method readMethod = property.getReadMethod();
 
                     MethodInfo mi;
@@ -849,7 +851,7 @@ public final class StorableGenerator<S extends Storable> {
                 }
 
                 // 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<S extends Storable> {
                     new HashMap<String, StorableProperty<S>>();
 
                 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<S extends Storable> {
 
         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<S extends Storable> {
         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<S extends Storable> {
         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<S extends Storable> {
         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<S extends Storable> {
         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<S extends Storable> {
         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<S extends Storable> {
         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<S extends Storable> {
         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<S extends Storable> {
         // Params to invoke String.equals.
         TypeDesc[] params = {TypeDesc.OBJECT};
 
+        Label derivedMatch = null;
         Label joinMatch = null;
 
         for (int i=0; i<caseCount; i++) {
@@ -2802,7 +2818,12 @@ public final class StorableGenerator<S extends Storable> {
                     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<S extends Storable> {
         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<S extends Storable> {
 
         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<S extends Storable> {
         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<S extends Storable> {
         // 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<S extends Storable> {
 
         StorableProperty<S>[] properties;
         {
-            // Exclude joins.
+            // Exclude derived properties and joins.
             List<StorableProperty<S>> list =
                 new ArrayList<StorableProperty<S>>(propertyMap.size());
             for (StorableProperty<S> 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<S> 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<S> index : indexes) {
+                    for (StorableProperty<S> 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<S>
                 (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<S>
             (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<S extends Storable> implements StorableProperty<S> {
+        private static final ChainedProperty[] EMPTY_CHAIN_ARRAY = new ChainedProperty[0];
+
         private final BeanProperty mBeanProperty;
         private final Class<S> 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<S>[] mDerivedFrom;
+
+        // Resolved derived to properties.
+        private ChainedProperty<S>[] mDerivedTo;
 
         SimpleProperty(BeanProperty property, Class<S> 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<S>[] 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<ChainedProperty<?>> derivedToSet = new LinkedHashSet<ChainedProperty<?>>();
+                Set<Class<?>> examinedSet = new HashSet<Class<?>>();
+
+                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<? extends Storable> getJoinedType() {
             return null;
         }
@@ -1635,6 +1820,216 @@ public class StorableIntrospector {
             app.append(getEnclosingType().getName());
             app.append('}');
         }
+
+        void resolveDerivedFrom(List<String> errorMessages, StorableInfo<S> 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<ChainedProperty<S>> derivedFromSet = new LinkedHashSet<ChainedProperty<S>>();
+
+            for (String fromName : fromNames) {
+                ChainedProperty<S> 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<String> errorMessages,
+                                            Set<ChainedProperty<S>> derivedFromSet,
+                                            ChainedProperty<S> from)
+        {
+            if (derivedFromSet.contains(from)) {
+                return false;
+            }
+
+            derivedFromSet.add(from);
+
+            ChainedProperty<S> 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<S> dep;
+                    if (trimmed == null) {
+                        dep = (ChainedProperty<S>) 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<? extends Storable> 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<count; i++) {
+                        if (!lastInChain.getInternalJoinElement(i).getName().equals
+                            (lastInChain.getExternalJoinElement(i).getName()))
+                        {
+                            naturalJoin = false;
+                            break;
+                        }
+                    }
+
+                    if (!naturalJoin) {
+                        suggest.append("(internal=");
+                        if (count > 1) {
+                            suggest.append('{');
+                        }
+                        for (int i=0; i<count; i++) {
+                            if (i > 0) {
+                                suggest.append(", ");
+                            }
+                            suggest.append('"');
+                            // This property's external is other's internal.
+                            suggest.append(lastInChain.getExternalJoinElement(i).getName());
+                            suggest.append('"');
+                        }
+                        if (count > 1) {
+                            suggest.append('}');
+                        }
+
+                        suggest.append(", external=");
+                        if (count > 1) {
+                            suggest.append('{');
+                        }
+                        for (int i=0; i<count; i++) {
+                            if (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<ChainedProperty<?>> derivedToSet,
+                                          Set<Class<?>> 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<S extends Storable> extends SimpleProperty<S> {
@@ -1649,16 +2044,19 @@ public class StorableIntrospector {
         private StorableProperty<S>[] mInternal;
         private StorableProperty<?>[] mExternal;
 
+        private boolean mOneToOne;
+
         JoinProperty(BeanProperty property, Class<S> enclosing,
                      boolean nullable,
                      String[] aliases, StorablePropertyConstraint[] constraints,
                      StorablePropertyAdapter adapter,
                      String sequence, boolean independent, boolean automatic,
+                     Derived derived,
                      Class<? extends Storable> 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<? extends Storable> 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<String> errorMessages, Map<String, StorableProperty<S>> properties) {
-            StorableInfo<?> joinedInfo = examine(getJoinedType());
+        void resolveJoin(List<String> errorMessages, StorableInfo<S> 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<String, ? extends StorableProperty<?>> primaryKeys;
+                    Map<String, ? extends StorableProperty<?>> 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<mInternalNames.length; i++) {
                 String internalName = mInternalNames[i];
-                StorableProperty property = properties.get(internalName);
+                StorableProperty property = info.getAllProperties().get(internalName);
                 if (property == null) {
                     errorMessages.add
                         ("Cannot find internal join element: \"" +
@@ -1958,12 +2367,12 @@ public class StorableIntrospector {
             // Test which keys of joined object are specified.
 
             // Create a copy of all the primary keys of joined object.
-            Set<StorableProperty> primaryKeys =
+            Set<StorableProperty> primaryKey =
                 new HashSet<StorableProperty>(joinedInfo.getPrimaryKeyProperties().values());
 
             // Remove external properties from the primary key set.
             for (int i=0; i<mInternal.length; i++) {
-                primaryKeys.remove(getExternalJoinElement(i));
+                primaryKey.remove(getExternalJoinElement(i));
             }
 
             // Do similar test for alternate keys.
@@ -1996,7 +2405,7 @@ public class StorableIntrospector {
             if (isQuery()) {
                 // Key of joined object must not be completely specified.
 
-                if (primaryKeys.size() <= 0) {
+                if (primaryKey.size() <= 0) {
                     errorMessages.add
                         ("Join property \"" + getName() +
                          "\" completely specifies primary key of joined object; " +
@@ -2019,7 +2428,7 @@ public class StorableIntrospector {
 
                 fullKeyCheck:
                 {
-                    if (primaryKeys.size() <= 0) {
+                    if (primaryKey.size() <= 0) {
                         break fullKeyCheck;
                     }
 
@@ -2035,6 +2444,48 @@ public class StorableIntrospector {
                          "declaring the property type as Query<" +
                          getJoinedType().getName() + '>');
                 }
+
+                // Determine if one-to-one join. If internal properties
+                // completely specify any key, then it is one-to-one.
+
+                boolean oneToOne = false;
+
+                oneToOneCheck: {
+                    Set<StorableProperty> internalPrimaryKey =
+                        new HashSet<StorableProperty>(info.getPrimaryKeyProperties().values());
+                
+                    for (int i=0; i<mInternal.length; i++) {
+                        internalPrimaryKey.remove(getInternalJoinElement(i));
+                        if (internalPrimaryKey.size() == 0) {
+                            oneToOne = true;
+                            break oneToOneCheck;
+                        }
+                    }
+
+                    altKeyScan:
+                    for (int i=0; i<info.getAlternateKeyCount(); i++) {
+                        Set<StorableProperty> altKey = new HashSet<StorableProperty>();
+
+                        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<mInternal.length; j++) {
+                            altKey.remove(getInternalJoinElement(j));
+                            if (altKey.size() == 0) {
+                                oneToOne = true;
+                                break oneToOneCheck;
+                            }
+                        }
+                    }
+                }
+
+                mOneToOne = oneToOne;
             }
 
             if (mutatorAllowed && getWriteMethod() == null) {
diff --git a/src/main/java/com/amazon/carbonado/info/StorableProperty.java b/src/main/java/com/amazon/carbonado/info/StorableProperty.java
index ce374ac..b3362c6 100644
--- a/src/main/java/com/amazon/carbonado/info/StorableProperty.java
+++ b/src/main/java/com/amazon/carbonado/info/StorableProperty.java
@@ -115,12 +115,19 @@ public interface StorableProperty<S extends Storable> 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<S extends Storable> 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<S>[] 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.
+     *
+     * <p>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<LayoutProperty> list = new ArrayList<LayoutProperty>(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<S extends Storable> implements Query<S>, 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<S extends Storable> extends AbstractQuery<S> {
         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<S extends Storable> {
         Map<String, ? extends StorableProperty<S>> map =
             StorableIntrospector.examine(mType).getDataProperties();
 
-        StorableProperty<S>[] properties = new StorableProperty[map.size()];
+        List<StorableProperty<S>> list = new ArrayList<StorableProperty<S>>(map.size());
 
-        int ordinal = 0;
         for (StorableProperty<S> 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<S> 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<S extends Storable> implements StorableCodec<S
             StorableIntrospector.examine(altStorable).getAllProperties();
 
         for (StorableProperty prop : currentProps.values()) {
-            if (prop.isJoin()) {
+            if (prop.isDerived() || prop.isJoin()) {
                 continue;
             }
 
diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/DependentStorableFetcher.java b/src/main/java/com/amazon/carbonado/repo/indexed/DependentStorableFetcher.java
new file mode 100644
index 0000000..4c24c04
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/repo/indexed/DependentStorableFetcher.java
@@ -0,0 +1,174 @@
+/*
+ * 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.repo.indexed;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.cojen.util.BeanPropertyAccessor;
+
+import com.amazon.carbonado.Cursor;
+import com.amazon.carbonado.FetchException;
+import com.amazon.carbonado.Query;
+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.filter.Filter;
+import com.amazon.carbonado.filter.RelOp;
+
+import com.amazon.carbonado.info.ChainedProperty;
+import com.amazon.carbonado.info.StorableProperty;
+
+/**
+ * Fetches Storables that have indexed derived-to properties which depend on S.
+ *
+ * @author Brian S O'Neill
+ */
+class DependentStorableFetcher<S extends Storable, D extends Storable> {
+    private final IndexedRepository mRepository;
+    private final IndexEntryAccessor<D>[] mIndexEntryAccessors;
+    private final Query<D> mQuery;
+    private final String[] mJoinProperties;
+    private final BeanPropertyAccessor mPropertyAccessor;
+
+    /**
+     * @param derivedTo special chained property from StorableProperty.getDerivedToProperties
+     */
+    DependentStorableFetcher(IndexedRepository repository,
+                             Class<S> sType, ChainedProperty<D> 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<D> dType = derivedTo.getPrimeProperty().getEnclosingType();
+
+        // Find the indexes that contain the prime derivedTo property.
+        List<IndexEntryAccessor<D>> accessorList = new ArrayList<IndexEntryAccessor<D>>();
+        for (IndexEntryAccessor<D> 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<S> join = (StorableProperty<S>) 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<D> dFilter = Filter.getOpenFilter(dType);
+        for (int i=0; i<joinElementCount; i++) {
+            StorableProperty<S> 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<D> fetchDependenentStorables(S storable) throws FetchException {
+        Query<D> 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<Storable> indexEntries) {
+        IndexEntryAccessor[] accessors = mIndexEntryAccessors;
+        int length = accessors.length;
+        for (int i=0; i<length; i++) {
+            IndexEntryAccessor accessor = accessors[i];
+            Storable indexEntry = accessor.getIndexEntryStorage().prepare();
+            accessor.copyFromMaster(indexEntry, master);
+            indexEntries.add(indexEntry);
+        }
+        return length;
+    }
+
+    @Override
+    public int hashCode() {
+        return mQuery.getFilter().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof DependentStorableFetcher) {
+            DependentStorableFetcher other = (DependentStorableFetcher) obj;
+            return mQuery.getFilter().equals(other.mQuery.getFilter())
+                && Arrays.equals(mJoinProperties, other.mJoinProperties)
+                && Arrays.equals(mIndexEntryAccessors, other.mIndexEntryAccessors);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "DependentStorableFetcher: {indexes=" + Arrays.toString(mIndexEntryAccessors) +
+            ", query=" + mQuery +
+            ", join properties=" + Arrays.toString(mJoinProperties) + '}';
+    }
+}
diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/DerivedIndexesTrigger.java b/src/main/java/com/amazon/carbonado/repo/indexed/DerivedIndexesTrigger.java
new file mode 100644
index 0000000..0a99fdd
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/repo/indexed/DerivedIndexesTrigger.java
@@ -0,0 +1,164 @@
+/*
+ * 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.repo.indexed;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.amazon.carbonado.Cursor;
+import com.amazon.carbonado.FetchException;
+import com.amazon.carbonado.PersistException;
+import com.amazon.carbonado.RepositoryException;
+import com.amazon.carbonado.Storable;
+import com.amazon.carbonado.Transaction;
+import com.amazon.carbonado.Trigger;
+
+import com.amazon.carbonado.info.ChainedProperty;
+
+/**
+ * Handles index updates for derived-to properties.
+ *
+ * @author Brian S O'Neill
+ */
+class DerivedIndexesTrigger<S extends Storable, D extends Storable> extends Trigger<S> {
+    private final DependentStorableFetcher<S, D> mFetcher;
+
+    /**
+     * @param derivedTo special chained property from StorableProperty.getDerivedToProperties
+     */
+    DerivedIndexesTrigger(IndexedRepository repository,
+                          Class<S> sType, ChainedProperty<D> derivedTo)
+        throws RepositoryException
+    {
+        this(new DependentStorableFetcher(repository, sType, derivedTo));
+    }
+
+    DerivedIndexesTrigger(DependentStorableFetcher<S, D> 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<Storable> createDependentIndexEntries(S storable) throws PersistException {
+        List<Storable> dependentIndexEntries = new ArrayList<Storable>();
+        createDependentIndexEntries(storable, dependentIndexEntries);
+        return dependentIndexEntries;
+    }
+
+    private void createDependentIndexEntries(S storable, List<Storable> 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<D> 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<Storable> oldIndexEntries = (List<Storable>) state;
+        int size = oldIndexEntries.size();
+
+        List<Storable> newIndexEntries = new ArrayList<Storable>(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<size; i++) {
+            Storable oldIndexEntry = oldIndexEntries.get(i);
+            Storable newIndexEntry = newIndexEntries.get(i);
+            if (!oldIndexEntry.equalProperties(newIndexEntry)) {
+                oldIndexEntry.delete();
+                newIndexEntry.tryInsert();
+            }
+        }
+    }
+}
diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexAnalysis.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexAnalysis.java
new file mode 100644
index 0000000..d0e3405
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexAnalysis.java
@@ -0,0 +1,132 @@
+/*
+ * 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.repo.indexed;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.amazon.carbonado.Storable;
+
+import com.amazon.carbonado.filter.Filter;
+import com.amazon.carbonado.filter.RelOp;
+
+import com.amazon.carbonado.info.ChainedProperty;
+import com.amazon.carbonado.info.Direction;
+import com.amazon.carbonado.info.StorableIndex;
+import com.amazon.carbonado.info.StorableInfo;
+import com.amazon.carbonado.info.StorableIntrospector;
+import com.amazon.carbonado.info.StorableProperty;
+
+import com.amazon.carbonado.qe.FilteringScore;
+
+import com.amazon.carbonado.spi.StorableIndexSet;
+
+/**
+ * Collection of static methods which perform index analysis.
+ *
+ * @author Brian S O'Neill
+ */
+class IndexAnalysis {
+    static <S extends Storable> StorableIndexSet<S> gatherDesiredIndexes(StorableInfo<S> info) {
+        StorableIndexSet<S> indexSet = new StorableIndexSet<S>();
+        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<S> 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<S> 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<S> 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<S> index =
+                new StorableIndex<S>(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<propertyCount; j++) {
+                if (index.getProperty(j).equals(property)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    static boolean isJoinAndUsedByIndexedDerivedProperty(StorableProperty<?> 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<ChainedProperty<?>> gatherDerivedToDependencies(StorableInfo<?> info) {
+        Set<ChainedProperty<?>> 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<ChainedProperty<?>>();
+                    }
+                    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<S extends Storable> 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<S> storableType)
         throws RepositoryException
     {
-        return ((IndexedStorage<S>) storageFor(storableType)).getIndexEntryAccessors();
+        Storage<S> storage = storageFor(storableType);
+        if (storage instanceof IndexedStorage) {
+            return ((IndexedStorage<S>) 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<S extends Storable> implements Storage<S>, StorageAccess<S> {
-    static <S extends Storable> StorableIndexSet<S> gatherDesiredIndexes(StorableInfo<S> info) {
-        StorableIndexSet<S> indexSet = new StorableIndexSet<S>();
-        indexSet.addIndexes(info);
-        indexSet.addAlternateKeys(info);
-        return indexSet;
-    }
-
     final IndexedRepository mRepository;
     final Storage<S> mMasterStorage;
 
@@ -102,7 +100,7 @@ class IndexedStorage<S extends Storable> implements Storage<S>, StorageAccess<S>
         // The set of indexes that the Storable defines, reduced.
         final StorableIndexSet<S> desiredIndexSet;
         {
-            desiredIndexSet = gatherDesiredIndexes(info);
+            desiredIndexSet = IndexAnalysis.gatherDesiredIndexes(info);
             desiredIndexSet.reduce(Direction.ASCENDING);
         }
 
@@ -299,6 +297,17 @@ class IndexedStorage<S extends Storable> implements Storage<S>, StorageAccess<S>
 
         mQueryableIndexSet = queryableIndexSet;
         mQueryEngine = new QueryEngine<S>(masterStorage.getStorableType(), repository);
+
+        // Install triggers to manage derived properties in external Storables.
+
+        Set<ChainedProperty<?>> derivedToDependencies =
+            IndexAnalysis.gatherDerivedToDependencies(info);
+
+        if (derivedToDependencies != null) {
+            for (ChainedProperty<?> derivedTo : derivedToDependencies) {
+                addTrigger(new DerivedIndexesTrigger(repository, getStorableType(), derivedTo));
+            }
+        }
     }
 
     public Class<S> 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<S extends Storable> implements IndexEntryAccessor<S> {
         }
     }
 
-    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<S extends Storable> {
         // UnsupportedOperationException.
         {
             for (JDBCStorableProperty<S> 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<S extends Storable> {
                 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<S extends Storable> {
             }
 
             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<S extends Storable> {
 
                 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<S extends Storable> {
                 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<String> errorMessages = new ArrayList<String>();
 
         for (StorableProperty<S> mainProperty : mainProperties.values()) {
-            if (mainProperty.isJoin() || tableName == null) {
+            if (mainProperty.isDerived() || mainProperty.isJoin() || tableName == null) {
                 jProperties.put(mainProperty.getName(), new JProperty<S>(mainProperty));
                 continue;
             }
@@ -1271,6 +1272,10 @@ public class JDBCStorableIntrospector extends StorableIntrospector {
             return mMainProperty.isJoin();
         }
 
+        public boolean isOneToOneJoin() {
+            return mMainProperty.isOneToOneJoin();
+        }
+
         public Class<? extends Storable> getJoinedType() {
             return mMainProperty.getJoinedType();
         }
@@ -1315,6 +1320,18 @@ public class JDBCStorableIntrospector extends StorableIntrospector {
             return mMainProperty.isIndependent();
         }
 
+        public boolean isDerived() {
+            return mMainProperty.isDerived();
+        }
+
+        public ChainedProperty<S>[] 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<S extends Storable> implements Query<S> {
         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<S> 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<S extends Storable>
                 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