Fix automatic lifecycle delete stacking 52/79952/12
authorRobert Varga <robert.varga@pantheon.tech>
Mon, 28 Jan 2019 13:19:00 +0000 (14:19 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 30 Jan 2019 07:49:34 +0000 (08:49 +0100)
Nodes with automatic lifecycle have the strange property that their
delete operations may actually stack with other operations.

Unfortunately delete operation itself has no semantics of having
underlying modifications, as it has no baseline to which to apply
them.

In order to fix this, we need to turn deletes into empty writes
for these constructs and once we are performing disappearing, undo
this trick. This effectively means that both empty writes and deletes
result in a delete operation, if they ended up modifying the tree,
or into a no-op if they did not.

JIRA: YANGTOOLS-938
Change-Id: I68b9f304e76d6b1bdb707f411a87cd6085ed8bc7
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
14 files changed:
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AbstractNodeContainerModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AbstractValueNodeModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AugmentationModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AutomaticLifecycleMixin.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ChoiceModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ListEntryModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ModifiedNode.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/OrderedMapModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/PresenceContainerModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/SchemaAwareApplyOperation.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/StructuralContainerModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UnkeyedListModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UnorderedMapModificationStrategy.java
yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/Bug2690Test.java

index d7666a7b10d17f6696ffc923a38e448ceda6c41d..b0d3310b4933b1fe6fefa1bc5e0a3e5bc18a2dfd 100644 (file)
@@ -78,9 +78,8 @@ abstract class AbstractNodeContainerModificationStrategy extends SchemaAwareAppl
     }
 
     @Override
-    protected TreeNode applyWrite(final ModifiedNode modification,
+    protected TreeNode applyWrite(final ModifiedNode modification, final NormalizedNode<?, ?> newValue,
             final Optional<TreeNode> currentMeta, final Version version) {
-        final NormalizedNode<?, ?> newValue = modification.getWrittenValue();
         final TreeNode newValueMeta = TreeNodeFactory.createTreeNode(newValue, version);
 
         if (modification.getChildren().isEmpty()) {
index 5037b99a3efdb677d4d3b0c5d3af20cd34c24df0..ff0e411677162250e4b5b51276243e08c7b1e5f5 100644 (file)
@@ -57,15 +57,16 @@ abstract class AbstractValueNodeModificationStrategy<T extends DataSchemaNode> e
     protected final TreeNode applyMerge(final ModifiedNode modification, final TreeNode currentMeta,
             final Version version) {
         // Just overwrite whatever was there, but be sure to run validation
-        verifyStructure(modification.getWrittenValue(), true);
+        final NormalizedNode<?, ?> newValue = modification.getWrittenValue();
+        verifyStructure(newValue, true);
         modification.resolveModificationType(ModificationType.WRITE);
-        return applyWrite(modification, null, version);
+        return applyWrite(modification, newValue, null, version);
     }
 
     @Override
-    protected final TreeNode applyWrite(final ModifiedNode modification,
+    protected final TreeNode applyWrite(final ModifiedNode modification, final NormalizedNode<?, ?> newValue,
             final Optional<TreeNode> currentMeta, final Version version) {
-        return TreeNodeFactory.createTreeNode(modification.getWrittenValue(), version);
+        return TreeNodeFactory.createTreeNode(newValue, version);
     }
 
     @Override
index 81a7b5941865f0c1d7b06ef692c70120b4e4bc15..0823e639c1717041ce4d26c9fef8c4848857edfb 100644 (file)
@@ -39,7 +39,8 @@ final class AugmentationModificationStrategy
     @Override
     Optional<TreeNode> apply(final ModifiedNode modification, final Optional<TreeNode> storeMeta,
             final Version version) {
-        return AutomaticLifecycleMixin.apply(super::apply, emptyNode, modification, storeMeta, version);
+        return AutomaticLifecycleMixin.apply(super::apply, this::applyWrite, emptyNode, modification, storeMeta,
+            version);
     }
 
     @Override
index a5b14fb2dac6e8c777beb1d759c98d579a0d8552..e547108c9627244d5465fa6aa1b6d0179c32f536 100644 (file)
@@ -30,6 +30,16 @@ final class AutomaticLifecycleMixin {
         Optional<TreeNode> apply(ModifiedNode modification, Optional<TreeNode> storeMeta, Version version);
     }
 
+    /**
+     * This is a capture of
+     * {@link SchemaAwareApplyOperation#applyWrite(ModifiedNode, NormalizedNode, Optional, Version)}.
+     */
+    @FunctionalInterface
+    interface ApplyWrite {
+        TreeNode applyWrite(ModifiedNode modification, NormalizedNode<?, ?> newValue, Optional<TreeNode> storeMeta,
+                Version version);
+    }
+
     /**
      * This is a capture of
      * {@link ModificationApplyOperation#checkApplicable(ModificationPath, NodeModification, Optional, Version)}.
@@ -52,53 +62,27 @@ final class AutomaticLifecycleMixin {
 
     }
 
-    static Optional<TreeNode> apply(final Apply delegate, final NormalizedNode<?, ?> emptyNode,
-            final ModifiedNode modification, final Optional<TreeNode> storeMeta, final Version version) {
-        final Optional<TreeNode> ret;
-        if (modification.getOperation() == LogicalOperation.TOUCH && !storeMeta.isPresent()) {
-            // Container is not present, let's take care of the 'magically appear' part of our job
-            ret = delegate.apply(modification, fakeMeta(emptyNode, version), version);
-
-            // Fake container got removed: that is a no-op
-            if (!ret.isPresent()) {
-                modification.resolveModificationType(ModificationType.UNMODIFIED);
-                return ret;
-            }
-
-            // If the delegate indicated SUBTREE_MODIFIED, account for the fake and report APPEARED
-            if (modification.getModificationType() == ModificationType.SUBTREE_MODIFIED) {
-                modification.resolveModificationType(ModificationType.APPEARED);
-            }
-        } else {
-            // Container is present, run normal apply operation
-            ret = delegate.apply(modification, storeMeta, version);
-
-            // Container was explicitly deleted, no magic required
-            if (!ret.isPresent()) {
-                return ret;
+    static Optional<TreeNode> apply(final Apply delegate, final ApplyWrite writeDelegate,
+            final NormalizedNode<?, ?> emptyNode, final ModifiedNode modification, final Optional<TreeNode> storeMeta,
+            final Version version) {
+        // The only way a tree node can disappear is through delete (which we handle here explicitly) or through
+        // actions of disappearResult(). It is therefore safe to perform Optional.get() on the results of
+        // delegate.apply()
+        final TreeNode ret;
+        if (modification.getOperation() == LogicalOperation.DELETE) {
+            if (modification.getChildren().isEmpty()) {
+                return delegate.apply(modification, storeMeta, version);
             }
-        }
-
-        /*
-         * At this point ret is guaranteed to be present. We need to take care of the 'magically disappear' part of
-         * our job. Check if there are any child nodes left. If there are none, remove this container and turn the
-         * modification into a DISAPPEARED.
-         */
-        final NormalizedNode<?, ?> data = ret.get().getData();
-        final boolean empty;
-        if (data instanceof NormalizedNodeContainer) {
-            empty = ((NormalizedNodeContainer<?, ?, ?>) data).getValue().isEmpty();
-        } else if (data instanceof OrderedNodeContainer) {
-            empty = ((OrderedNodeContainer<?>) data).getSize() == 0;
+            // Delete with children, implies it really is an empty write
+            ret = writeDelegate.applyWrite(modification, emptyNode, storeMeta, version);
+        } else if (modification.getOperation() == LogicalOperation.TOUCH && !storeMeta.isPresent()) {
+            ret = applyTouch(delegate, emptyNode, modification, storeMeta, version);
         } else {
-            throw new IllegalStateException("Unhandled data " + data);
+            // No special handling required here, run normal apply operation
+            ret = delegate.apply(modification, storeMeta, version).get();
         }
 
-        if (empty) {
-            modification.resolveModificationType(ModificationType.DISAPPEARED);
-            return Optional.empty();
-        }
-        return ret;
+        return disappearResult(modification, ret, storeMeta);
     }
 
     static void checkApplicable(final CheckApplicable delegate, final NormalizedNode<?, ?> emptyNode,
@@ -112,7 +96,53 @@ final class AutomaticLifecycleMixin {
         }
     }
 
+    private static TreeNode applyTouch(final Apply delegate, final NormalizedNode<?, ?> emptyNode,
+            final ModifiedNode modification, final Optional<TreeNode> storeMeta, final Version version) {
+        // Container is not present, let's take care of the 'magically appear' part of our job
+        final Optional<TreeNode> ret = delegate.apply(modification, fakeMeta(emptyNode, version), version);
+
+        // If the delegate indicated SUBTREE_MODIFIED, account for the fake and report APPEARED
+        if (modification.getModificationType() == ModificationType.SUBTREE_MODIFIED) {
+            modification.resolveModificationType(ModificationType.APPEARED);
+        }
+        return ret.get();
+    }
+
+    private static Optional<TreeNode> disappearResult(final ModifiedNode modification, final TreeNode result,
+            final Optional<TreeNode> storeMeta) {
+        // Check if the result is in fact empty before pulling any tricks
+        if (!isEmpty(result)) {
+            return Optional.of(result);
+        }
+
+        // We are pulling the 'disappear' trick, but what we report can be three different things
+        final ModificationType finalType;
+        if (!storeMeta.isPresent()) {
+            // ... there was nothing in the datastore, no change
+            finalType = ModificationType.UNMODIFIED;
+        } else if (modification.getModificationType() == ModificationType.WRITE) {
+            // ... this was an empty write, possibly originally a delete
+            finalType = ModificationType.DELETE;
+        } else {
+            // ... it really disappeared
+            finalType = ModificationType.DISAPPEARED;
+        }
+        modification.resolveModificationType(finalType);
+        return Optional.empty();
+    }
+
     private static Optional<TreeNode> fakeMeta(final NormalizedNode<?, ?> emptyNode, final Version version) {
         return Optional.of(TreeNodeFactory.createTreeNode(emptyNode, version));
     }
+
+    private static boolean isEmpty(final TreeNode treeNode) {
+        final NormalizedNode<?, ?> data = treeNode.getData();
+        if (data instanceof NormalizedNodeContainer) {
+            return ((NormalizedNodeContainer<?, ?, ?>) data).getValue().isEmpty();
+        }
+        if (data instanceof OrderedNodeContainer) {
+            return ((OrderedNodeContainer<?>) data).getSize() == 0;
+        }
+        throw new IllegalStateException("Unhandled data " + data);
+    }
 }
index 125fa1f92ed3962752119f85ed9453b68475cfce..0538e07e6b9f2824db1e5e745071d046e6705752 100644 (file)
@@ -78,11 +78,11 @@ final class ChoiceModificationStrategy extends AbstractNodeContainerModification
         emptyNode = ImmutableNodes.choiceNode(schema.getQName());
     }
 
-
     @Override
     Optional<TreeNode> apply(final ModifiedNode modification, final Optional<TreeNode> storeMeta,
             final Version version) {
-        return AutomaticLifecycleMixin.apply(super::apply, emptyNode, modification, storeMeta, version);
+        return AutomaticLifecycleMixin.apply(super::apply, this::applyWrite, emptyNode, modification, storeMeta,
+            version);
     }
 
     @Override
@@ -149,9 +149,9 @@ final class ChoiceModificationStrategy extends AbstractNodeContainerModification
     }
 
     @Override
-    protected TreeNode applyWrite(final ModifiedNode modification, final Optional<TreeNode> currentMeta,
-            final Version version) {
-        final TreeNode ret = super.applyWrite(modification, currentMeta, version);
+    protected TreeNode applyWrite(final ModifiedNode modification,  final NormalizedNode<?, ?> newValue,
+            final Optional<TreeNode> currentMeta, final Version version) {
+        final TreeNode ret = super.applyWrite(modification, newValue, currentMeta, version);
         enforceCases(ret);
         return ret;
     }
index d64442ba57cd21fe1f12c7cae1ef1f3744b1f933..3712106d83abf1c00d2509303bee824f71c48fe1 100644 (file)
@@ -43,9 +43,9 @@ final class ListEntryModificationStrategy extends AbstractDataNodeContainerModif
     }
 
     @Override
-    protected TreeNode applyWrite(final ModifiedNode modification, final Optional<TreeNode> currentMeta,
-            final Version version) {
-        final TreeNode ret = super.applyWrite(modification, currentMeta, version);
+    protected TreeNode applyWrite(final ModifiedNode modification, final NormalizedNode<?, ?> newValue,
+            final Optional<TreeNode> currentMeta, final Version version) {
+        final TreeNode ret = super.applyWrite(modification, newValue, currentMeta, version);
         enforcer.enforceOnTreeNode(ret);
         return ret;
     }
index 1b02ea7fc3328c0262d91dba44150eb5d354b151..9f648783eb65438fc6ee84398b350682066cb835 100644 (file)
@@ -7,6 +7,7 @@
  */
 package org.opendaylight.yangtools.yang.data.impl.schema.tree;
 
+import static com.google.common.base.Verify.verifyNotNull;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.MoreObjects;
@@ -103,8 +104,9 @@ final class ModifiedNode extends NodeModification implements StoreTreeNode<Modif
      *
      * @return Currently-written value
      */
+    @Nonnull
     NormalizedNode<?, ?> getWrittenValue() {
-        return value;
+        return verifyNotNull(value);
     }
 
     /**
index ce9db82652495ed8be469649cc08a408304280c3..f68efd4ef8aaea8c1fc45f6c89dfc198b89a8cd0 100644 (file)
@@ -37,7 +37,8 @@ final class OrderedMapModificationStrategy extends AbstractNodeContainerModifica
     @Override
     Optional<TreeNode> apply(final ModifiedNode modification, final Optional<TreeNode> storeMeta,
             final Version version) {
-        return AutomaticLifecycleMixin.apply(super::apply, emptyNode, modification, storeMeta, version);
+        return AutomaticLifecycleMixin.apply(super::apply, this::applyWrite, emptyNode, modification, storeMeta,
+            version);
     }
 
     @Override
index 578c50de2458c820367996df655ac6af8c543b57..5be8745cad758f1623a76030a250261c699d67a1 100644 (file)
@@ -44,9 +44,9 @@ final class PresenceContainerModificationStrategy extends ContainerModificationS
     }
 
     @Override
-    protected TreeNode applyWrite(final ModifiedNode modification, final Optional<TreeNode> currentMeta,
-            final Version version) {
-        final TreeNode ret = super.applyWrite(modification, currentMeta, version);
+    protected TreeNode applyWrite(final ModifiedNode modification, final NormalizedNode<?, ?> newValue,
+            final Optional<TreeNode> currentMeta, final Version version) {
+        final TreeNode ret = super.applyWrite(modification, newValue, currentMeta, version);
         enforcer.enforceOnTreeNode(ret);
         return ret;
     }
index eeb6a996e7c87f0c1b37a9858898c6edea4cfd5a..6b2b10aea20f7e3ab8b3087f57d7257f053929c8 100644 (file)
@@ -8,12 +8,14 @@
 package org.opendaylight.yangtools.yang.data.impl.schema.tree;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Verify.verifyNotNull;
 
 import java.util.List;
 import java.util.Optional;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.ConflictingModificationAppliedException;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeConfiguration;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
@@ -216,7 +218,7 @@ abstract class SchemaAwareApplyOperation extends ModificationApplyOperation {
                     // structure is usually verified when the transaction is sealed. To preserve correctness, we have
                     // to run that validation here.
                     modification.resolveModificationType(ModificationType.WRITE);
-                    result = applyWrite(modification, currentMeta, version);
+                    result = applyWrite(modification, modification.getWrittenValue(), currentMeta, version);
                     verifyStructure(result.getData(), true);
                 } else {
                     result = applyMerge(modification, currentMeta.get(), version);
@@ -225,7 +227,8 @@ abstract class SchemaAwareApplyOperation extends ModificationApplyOperation {
                 return modification.setSnapshot(Optional.of(result));
             case WRITE:
                 modification.resolveModificationType(ModificationType.WRITE);
-                return modification.setSnapshot(Optional.of(applyWrite(modification, currentMeta, version)));
+                return modification.setSnapshot(Optional.of(applyWrite(modification,
+                    verifyNotNull(modification.getWrittenValue()), currentMeta, version)));
             case NONE:
                 modification.resolveModificationType(ModificationType.UNMODIFIED);
                 return currentMeta;
@@ -246,7 +249,8 @@ abstract class SchemaAwareApplyOperation extends ModificationApplyOperation {
      */
     protected abstract TreeNode applyMerge(ModifiedNode modification, TreeNode currentMeta, Version version);
 
-    protected abstract TreeNode applyWrite(ModifiedNode modification, Optional<TreeNode> currentMeta, Version version);
+    protected abstract TreeNode applyWrite(ModifiedNode modification, NormalizedNode<?, ?> newValue,
+            Optional<TreeNode> currentMeta, Version version);
 
     /**
      * Apply a nested operation. Since there may not actually be a nested operation
index 3ba6ad9d5d707cc7eda30b51a1fca416deb3eb03..5049c18e96ea7be28ed999b44c24086db00e79a2 100644 (file)
@@ -35,7 +35,8 @@ final class StructuralContainerModificationStrategy extends ContainerModificatio
     @Override
     Optional<TreeNode> apply(final ModifiedNode modification, final Optional<TreeNode> storeMeta,
             final Version version) {
-        return AutomaticLifecycleMixin.apply(super::apply, emptyNode, modification, storeMeta, version);
+        return AutomaticLifecycleMixin.apply(super::apply, this::applyWrite, emptyNode, modification, storeMeta,
+            version);
     }
 
     @Override
index 4b6c6cecc9d7272edcb4cda4a173f95feba38227..50280573a08d36ba368711f7b9755c942af86cbd 100644 (file)
@@ -38,7 +38,8 @@ final class UnkeyedListModificationStrategy extends SchemaAwareApplyOperation {
     @Override
     Optional<TreeNode> apply(final ModifiedNode modification, final Optional<TreeNode> storeMeta,
             final Version version) {
-        return AutomaticLifecycleMixin.apply(super::apply, emptyNode, modification, storeMeta, version);
+        return AutomaticLifecycleMixin.apply(super::apply, this::applyWrite, emptyNode, modification, storeMeta,
+            version);
     }
 
     @Override
@@ -60,17 +61,14 @@ final class UnkeyedListModificationStrategy extends SchemaAwareApplyOperation {
     }
 
     @Override
-    protected TreeNode applyTouch(final ModifiedNode modification,
-            final TreeNode currentMeta, final Version version) {
+    protected TreeNode applyTouch(final ModifiedNode modification, final TreeNode currentMeta, final Version version) {
         throw new UnsupportedOperationException("UnkeyedList does not support subtree change.");
     }
 
     @Override
-    protected TreeNode applyWrite(final ModifiedNode modification,
+    protected TreeNode applyWrite(final ModifiedNode modification, final NormalizedNode<?, ?> newValue,
             final Optional<TreeNode> currentMeta, final Version version) {
-        final NormalizedNode<?, ?> newValue = modification.getWrittenValue();
         final TreeNode newValueMeta = TreeNodeFactory.createTreeNode(newValue, version);
-
         if (modification.getChildren().isEmpty()) {
             return newValueMeta;
         }
index d49e46a26548bcc6aa0ddb9063594904b1149ff5..dc7a9c372da17d82f89ca08b28313129aab5df7a 100644 (file)
@@ -36,7 +36,8 @@ final class UnorderedMapModificationStrategy extends AbstractNodeContainerModifi
     @Override
     Optional<TreeNode> apply(final ModifiedNode modification, final Optional<TreeNode> storeMeta,
             final Version version) {
-        return AutomaticLifecycleMixin.apply(super::apply, emptyNode, modification, storeMeta, version);
+        return AutomaticLifecycleMixin.apply(super::apply, this::applyWrite, emptyNode, modification, storeMeta,
+            version);
     }
 
     @Override
index 3d744ecb6b1214bd28e9ce841e70913bab417767..2dbe0b45b38f9e1cc45fc7ff8b055d18ca6c96b4 100644 (file)
@@ -14,6 +14,7 @@ import java.util.Optional;
 import org.junit.Before;
 import org.junit.Test;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
@@ -29,6 +30,9 @@ import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
 
 public class Bug2690Test extends AbstractTestModelTest {
+    private static final YangInstanceIdentifier NAME_PATH = YangInstanceIdentifier.of(TestModel.NON_PRESENCE_QNAME)
+            .node(TestModel.NAME_QNAME);
+
     private DataTree inMemoryDataTree;
 
     @Before
@@ -42,28 +46,24 @@ public class Bug2690Test extends AbstractTestModelTest {
         final MapEntryNode fooEntryNode = ImmutableNodes.mapEntry(TestModel.OUTER_LIST_QNAME, TestModel.ID_QNAME, 1);
         final MapEntryNode barEntryNode = ImmutableNodes.mapEntry(TestModel.OUTER_LIST_QNAME, TestModel.ID_QNAME, 2);
         final MapNode mapNode1 = ImmutableNodes.mapNodeBuilder()
-                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TestModel.OUTER_LIST_QNAME))
+                .withNodeIdentifier(new NodeIdentifier(TestModel.OUTER_LIST_QNAME))
                 .withChild(fooEntryNode).build();
         final MapNode mapNode2 = ImmutableNodes.mapNodeBuilder()
-                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TestModel.OUTER_LIST_QNAME))
+                .withNodeIdentifier(new NodeIdentifier(TestModel.OUTER_LIST_QNAME))
                 .withChild(barEntryNode).build();
 
         final ContainerNode cont1 = Builders.containerBuilder()
-                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TestModel.TEST_QNAME))
+                .withNodeIdentifier(new NodeIdentifier(TestModel.TEST_QNAME))
                 .withChild(mapNode1).build();
 
         final ContainerNode cont2 = Builders.containerBuilder()
-                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TestModel.TEST_QNAME))
+                .withNodeIdentifier(new NodeIdentifier(TestModel.TEST_QNAME))
                 .withChild(mapNode2).build();
 
         final DataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
         modificationTree.write(TestModel.TEST_PATH, cont1);
         modificationTree.merge(TestModel.TEST_PATH, cont2);
-        modificationTree.ready();
-
-        inMemoryDataTree.validate(modificationTree);
-        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
-        inMemoryDataTree.commit(prepare);
+        commit(modificationTree);
 
         final DataTreeSnapshot snapshotAfterTx = inMemoryDataTree.takeSnapshot();
         final DataTreeModification modificationAfterTx = snapshotAfterTx.newModification();
@@ -71,4 +71,38 @@ public class Bug2690Test extends AbstractTestModelTest {
         assertTrue(readNode.isPresent());
         assertEquals(2, ((NormalizedNodeContainer<?,?,?>)readNode.get()).getValue().size());
     }
+
+    @Test
+    public void testDeleteStructuralAndWriteChild() throws DataValidationFailedException {
+        final DataTreeModification modificationTree = setupTestDeleteStructuralAndWriteChild();
+        verifyTestDeleteStructuralAndWriteChild(modificationTree);
+    }
+
+    @Test
+    public void testDeleteStructuralAndWriteChildWithCommit() throws DataValidationFailedException {
+        final DataTreeModification modificationTree = setupTestDeleteStructuralAndWriteChild();
+        commit(modificationTree);
+        verifyTestDeleteStructuralAndWriteChild(inMemoryDataTree.takeSnapshot());
+    }
+
+    private DataTreeModification setupTestDeleteStructuralAndWriteChild() {
+        final DataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree.delete(YangInstanceIdentifier.of(TestModel.NON_PRESENCE_QNAME));
+        modificationTree.write(NAME_PATH, Builders.leafBuilder()
+            .withNodeIdentifier(new NodeIdentifier(TestModel.NAME_QNAME)).withValue("abc").build());
+        return modificationTree;
+    }
+
+    private static void verifyTestDeleteStructuralAndWriteChild(final DataTreeSnapshot snapshot) {
+        final Optional<NormalizedNode<?, ?>> readNode = snapshot.readNode(NAME_PATH);
+        assertTrue(readNode.isPresent());
+    }
+
+    private void commit(final DataTreeModification modificationTree) throws DataValidationFailedException {
+        modificationTree.ready();
+
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
 }