Correctly support NULLs on nested entity types

Nested entities need to be recursively handled, as the Java object
instance must be allocated before fetch starts and destroyed if
fetch produces NULLs.  During insert/update we need to not enter
the nested object if it is null, but instead bind all fields to
their NULL value.

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gwtorm/jdbc/gen/AccessGen.java b/src/main/java/com/google/gwtorm/jdbc/gen/AccessGen.java
index 908fe48..b719847 100644
--- a/src/main/java/com/google/gwtorm/jdbc/gen/AccessGen.java
+++ b/src/main/java/com/google/gwtorm/jdbc/gen/AccessGen.java
@@ -293,38 +293,7 @@
       cols.addAll(model.getDependentFields());
       cols.addAll(model.getRowVersionFields());
       for (final ColumnModel field : cols) {
-        if (field.isNested() && field.isNotNull()) {
-          for (final ColumnModel c : field.getAllLeafColumns()) {
-            cgs.setFieldReference(c);
-            dialect.getSqlTypeInfo(c).generatePreparedStatementSet(cgs);
-          }
-        } else if (field.isNested()) {
-          final int colIdx = cgs.getColumnIndex();
-          final Label isnull = new Label();
-          final Label end = new Label();
-
-          cgs.setFieldReference(field);
-          cgs.pushFieldValue();
-          mv.visitJumpInsn(IFNULL, isnull);
-          cgs.resetColumnIndex(colIdx);
-          for (final ColumnModel c : field.getAllLeafColumns()) {
-            cgs.setFieldReference(c);
-            dialect.getSqlTypeInfo(c).generatePreparedStatementSet(cgs);
-          }
-          mv.visitJumpInsn(GOTO, end);
-
-          mv.visitLabel(isnull);
-          cgs.resetColumnIndex(colIdx);
-          for (final ColumnModel c : field.getAllLeafColumns()) {
-            cgs.setFieldReference(c);
-            dialect.getSqlTypeInfo(c).generatePreparedStatementNull(cgs);
-          }
-
-          mv.visitLabel(end);
-        } else {
-          cgs.setFieldReference(field);
-          dialect.getSqlTypeInfo(field).generatePreparedStatementSet(cgs);
-        }
+        doBindOne(mv, cgs, field);
       }
     }
 
@@ -349,6 +318,41 @@
     mv.visitEnd();
   }
 
+  private void doBindOne(final MethodVisitor mv, final CodeGenSupport cgs,
+      final ColumnModel field) {
+    if (field.isNested() && field.isNotNull()) {
+      for (final ColumnModel c : field.getAllLeafColumns()) {
+        doBindOne(mv, cgs, c);
+      }
+
+    } else if (field.isNested()) {
+      final int colIdx = cgs.getColumnIndex();
+      final Label isnull = new Label();
+      final Label end = new Label();
+
+      cgs.setFieldReference(field);
+      cgs.pushFieldValue();
+      mv.visitJumpInsn(IFNULL, isnull);
+      cgs.resetColumnIndex(colIdx);
+      for (final ColumnModel c : field.getNestedColumns()) {
+        doBindOne(mv, cgs, c);
+      }
+      mv.visitJumpInsn(GOTO, end);
+
+      mv.visitLabel(isnull);
+      cgs.resetColumnIndex(colIdx);
+      for (final ColumnModel c : field.getAllLeafColumns()) {
+        cgs.setFieldReference(c);
+        dialect.getSqlTypeInfo(c).generatePreparedStatementNull(cgs);
+      }
+
+      mv.visitLabel(end);
+    } else {
+      cgs.setFieldReference(field);
+      dialect.getSqlTypeInfo(field).generatePreparedStatementSet(cgs);
+    }
+  }
+
   private void implementBindOneFetch() {
     final MethodVisitor mv =
         cw.visitMethod(ACC_PUBLIC | ACC_FINAL, "bindOneFetch", Type
@@ -385,43 +389,7 @@
     cols.addAll(model.getRowVersionFields());
     cols.addAll(model.getPrimaryKeyColumns());
     for (final ColumnModel field : cols) {
-      if (field.isNested()) {
-        int oldIdx = cgs.getColumnIndex();
-        final Type vType = CodeGenSupport.toType(field);
-
-        cgs.setFieldReference(field);
-        cgs.fieldSetBegin();
-        mv.visitTypeInsn(NEW, vType.getInternalName());
-        mv.visitInsn(DUP);
-        mv.visitMethodInsn(INVOKESPECIAL, vType.getInternalName(), "<init>",
-            Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {}));
-        cgs.fieldSetEnd();
-
-        cgs.resetColumnIndex(oldIdx);
-        for (final ColumnModel c : field.getAllLeafColumns()) {
-          cgs.setFieldReference(c);
-          dialect.getSqlTypeInfo(c).generateResultSetGet(cgs);
-        }
-
-        if (!field.isNotNull()) {
-          final Label islive = new Label();
-          cgs.pushSqlHandle();
-          mv.visitMethodInsn(INVOKEINTERFACE, Type.getType(ResultSet.class)
-              .getInternalName(), "wasNull", Type.getMethodDescriptor(
-              Type.BOOLEAN_TYPE, new Type[] {}));
-          mv.visitJumpInsn(IFEQ, islive);
-          oldIdx = cgs.getColumnIndex();
-          cgs.setFieldReference(field);
-          cgs.fieldSetBegin();
-          mv.visitInsn(ACONST_NULL);
-          cgs.fieldSetEnd();
-          cgs.resetColumnIndex(oldIdx);
-          mv.visitLabel(islive);
-        }
-      } else {
-        cgs.setFieldReference(field);
-        dialect.getSqlTypeInfo(field).generateResultSetGet(cgs);
-      }
+      doFetchOne(mv, cgs, field, -1);
     }
 
     mv.visitInsn(RETURN);
@@ -429,6 +397,91 @@
     mv.visitEnd();
   }
 
+  private void doFetchOne(final MethodVisitor mv, final CodeGenSupport cgs,
+      final ColumnModel field, final int reportLiveInto) {
+    if (field.isNested()) {
+      int oldIdx = cgs.getColumnIndex();
+      final Type vType = CodeGenSupport.toType(field);
+      final int livecnt;
+
+      if (field.isNotNull()) {
+        livecnt = -1;
+      } else {
+        livecnt = cgs.newLocal();
+        cgs.push(0);
+        mv.visitVarInsn(ISTORE, livecnt);
+      }
+
+      cgs.setFieldReference(field);
+      cgs.fieldSetBegin();
+      mv.visitTypeInsn(NEW, vType.getInternalName());
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, vType.getInternalName(), "<init>", Type
+          .getMethodDescriptor(Type.VOID_TYPE, new Type[] {}));
+      cgs.fieldSetEnd();
+
+      cgs.resetColumnIndex(oldIdx);
+      for (final ColumnModel c : field.getNestedColumns()) {
+        doFetchOne(mv, cgs, c, livecnt);
+      }
+
+      if (livecnt >= 0) {
+        oldIdx = cgs.getColumnIndex();
+
+        final Label islive = new Label();
+        mv.visitVarInsn(ILOAD, livecnt);
+        mv.visitJumpInsn(IFNE, islive);
+        cgs.setFieldReference(field);
+        cgs.fieldSetBegin();
+        mv.visitInsn(ACONST_NULL);
+        cgs.fieldSetEnd();
+
+        if (reportLiveInto >= 0) {
+          final Label end = new Label();
+          mv.visitJumpInsn(GOTO, end);
+          mv.visitLabel(islive);
+          mv.visitIincInsn(reportLiveInto, 1);
+          mv.visitLabel(end);
+        } else {
+          mv.visitLabel(islive);
+        }
+
+        cgs.resetColumnIndex(oldIdx);
+        cgs.freeLocal(livecnt);
+      }
+
+    } else {
+      final int dupTo;
+      if (reportLiveInto >= 0
+          && CodeGenSupport.toType(field).getSort() == Type.OBJECT) {
+        dupTo = cgs.newLocal();
+      } else {
+        dupTo = -1;
+      }
+
+      cgs.setFieldReference(field);
+      cgs.setDupOnFieldSetEnd(dupTo);
+      dialect.getSqlTypeInfo(field).generateResultSetGet(cgs);
+
+      if (reportLiveInto >= 0) {
+        final Label wasnull = new Label();
+        if (dupTo >= 0) {
+          mv.visitVarInsn(ALOAD, dupTo);
+          mv.visitJumpInsn(IFNULL, wasnull);
+          cgs.freeLocal(dupTo);
+        } else {
+          cgs.pushSqlHandle();
+          mv.visitMethodInsn(INVOKEINTERFACE, Type.getType(ResultSet.class)
+              .getInternalName(), "wasNull", Type.getMethodDescriptor(
+              Type.BOOLEAN_TYPE, new Type[] {}));
+          mv.visitJumpInsn(IFNE, wasnull);
+        }
+        mv.visitIincInsn(reportLiveInto, 1);
+        mv.visitLabel(wasnull);
+      }
+    }
+  }
+
   private void implementKeyQuery(final KeyModel info) {
     final Type keyType = CodeGenSupport.toType(info.getField());
     final StringBuilder query = new StringBuilder();
diff --git a/src/main/java/com/google/gwtorm/jdbc/gen/CodeGenSupport.java b/src/main/java/com/google/gwtorm/jdbc/gen/CodeGenSupport.java
index 3fc40a3..2d78ccc 100644
--- a/src/main/java/com/google/gwtorm/jdbc/gen/CodeGenSupport.java
+++ b/src/main/java/com/google/gwtorm/jdbc/gen/CodeGenSupport.java
@@ -23,13 +23,19 @@
 import java.lang.reflect.Method;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
+import java.util.ArrayList;
+import java.util.List;
 
 public class CodeGenSupport implements Opcodes {
   public final MethodVisitor mv;
   private ColumnModel col;
+  private int dupOnSet;
   private int columnIdx;
   private Type entityType;
 
+  private int lastLocal = 2;
+  private List<Integer> freeLocals = new ArrayList<Integer>(4);
+
   public CodeGenSupport(final MethodVisitor method) {
     mv = method;
   }
@@ -73,12 +79,24 @@
     mv.visitVarInsn(type.getOpcode(ILOAD), index);
   }
 
+  public int newLocal() {
+    if (freeLocals.isEmpty()) {
+      return ++lastLocal;
+    }
+    return freeLocals.remove(freeLocals.size() - 1);
+  }
+
+  public void freeLocal(final int index) {
+    freeLocals.add(index);
+  }
+
   public void setEntityType(final Type et) {
     entityType = et;
   }
 
   public void setFieldReference(final ColumnModel cm) {
     col = cm;
+    dupOnSet = -1;
     columnIdx++;
   }
 
@@ -145,10 +163,18 @@
 
   public void fieldSetEnd() {
     final Type c = containerClass(col);
+    if (dupOnSet >= 0) {
+      mv.visitInsn(DUP);
+      mv.visitVarInsn(ASTORE, dupOnSet);
+    }
     mv.visitFieldInsn(PUTFIELD, c.getInternalName(), col.getFieldName(),
         toType(col).getDescriptor());
   }
 
+  public void setDupOnFieldSetEnd(final int varIdx) {
+    dupOnSet = varIdx;
+  }
+
   public void pushFieldValue() {
     pushEntity();
     appendGetField(col);