Repository: dmlc/dgl

Issue:
When an ntype is empty, g.add_nodes() overwrites prior features instead of initializing them
## 🐛 Bug

If there are no nodes or edges of a particular type, adding them with g.add_nodes() or g.add_edges() *with data* will result in the feature schema for that type being erased and overwritten.

## To Reproduce

Steps to reproduce the behavior:

```python
g = dgl.graph(([], []))
g.ndata["x"] = torch.tensor([])
g.ndata["y"] = torch.tensor([])
g.ndata
> {'x': tensor([]), 'y': torch.tensor([])}
g.add_nodes(1, {'y': torch.tensor([0])})
g.ndata
{'y': tensor([0])
```

Same thing for edges.

## Expected behavior

If a graph specifies features for an ntype or etype, even if there are no nodes or edges yet, new nodes and edges added to the graph should initialize those features instead of erasing them.

## Environment

 - DGL Version (e.g., 1.0): 0.5.3
 - Backend Library & Version (e.g., PyTorch 0.4.1, MXNet/Gluon 1.3): PyTorch 1.6.0
 - How you installed DGL (`conda`, `pip`, source): conda
 - Python version: 3.7



Discussion:

Resolving patch:
diff --git a/python/dgl/frame.py b/python/dgl/frame.py
--- a/python/dgl/frame.py
+++ b/python/dgl/frame.py
@@ -505,31 +505,25 @@ def update_row(self, rowids, data):
 
     def _append(self, other):
         """Append ``other`` frame to ``self`` frame."""
-        # NOTE: `other` can be empty.
-        if self.num_rows == 0:
-            # if no rows in current frame; append is equivalent to
-            # directly updating columns.
-            self._columns = {key: Column.create(data) for key, data in other.items()}
-        else:
-            # pad columns that are not provided in the other frame with initial values
-            for key, col in self._columns.items():
-                if key in other:
-                    continue
-                scheme = col.scheme
-                ctx = F.context(col.data)
-                if self.get_initializer(key) is None:
-                    self._set_zero_default_initializer()
-                initializer = self.get_initializer(key)
-                new_data = initializer((other.num_rows,) + scheme.shape,
-                                       scheme.dtype, ctx,
-                                       slice(self._num_rows, self._num_rows + other.num_rows))
-                other[key] = new_data
-            # append other to self
-            for key, col in other._columns.items():
-                if key not in self._columns:
-                    # the column does not exist; init a new column
-                    self.add_column(key, col.scheme, F.context(col.data))
-                self._columns[key].extend(col.data, col.scheme)
+        # pad columns that are not provided in the other frame with initial values
+        for key, col in self._columns.items():
+            if key in other:
+                continue
+            scheme = col.scheme
+            ctx = F.context(col.data)
+            if self.get_initializer(key) is None:
+                self._set_zero_default_initializer()
+            initializer = self.get_initializer(key)
+            new_data = initializer((other.num_rows,) + scheme.shape,
+                                   scheme.dtype, ctx,
+                                   slice(self._num_rows, self._num_rows + other.num_rows))
+            other[key] = new_data
+        # append other to self
+        for key, col in other._columns.items():
+            if key not in self._columns:
+                # the column does not exist; init a new column
+                self.add_column(key, col.scheme, F.context(col.data))
+            self._columns[key].extend(col.data, col.scheme)
 
     def append(self, other):
         """Append another frame's data into this frame.


Test patch:
diff --git a/tests/compute/test_graph.py b/tests/compute/test_graph.py
--- a/tests/compute/test_graph.py
+++ b/tests/compute/test_graph.py
@@ -338,6 +338,14 @@ def test_hypersparse_query():
     assert g.out_degree(0) == 1
     assert g.out_degree(1) == 0
 
+def test_empty_data_initialized():
+    g = dgl.DGLGraph()
+    g = g.to(F.ctx())
+    g.ndata["ha"] = F.tensor([])
+    g.add_nodes(1, {"hb": F.tensor([1])})
+    assert "ha" in g.ndata
+    assert len(g.ndata["ha"]) == 1
+
 if __name__ == '__main__':
     test_query()
     test_mutation()